├── docs ├── static │ ├── .nojekyll │ └── img │ │ ├── copy.svg │ │ ├── favicon.svg │ │ ├── copy-success.svg │ │ ├── sponsor.svg │ │ ├── github.svg │ │ ├── open-source.svg │ │ └── install-bg.svg ├── babel.config.js ├── src │ ├── pages │ │ ├── markdown-page.md │ │ └── index.js │ └── components │ │ ├── Demo │ │ ├── LeaveDetection.js │ │ ├── ClickOutside.js │ │ ├── Debounce.js │ │ └── Hold.js │ │ └── Animations │ │ ├── AnimatedContainer.js │ │ └── SplitText.js ├── docs │ ├── hooks │ │ ├── _category_.json │ │ ├── useHover.mdx │ │ ├── useFavicon.mdx │ │ ├── usePrefersTheme.mdx │ │ ├── useTitle.mdx │ │ ├── useUrgentUpdate.mdx │ │ ├── useScrollDevice.mdx │ │ ├── useLeaveDetection.mdx │ │ ├── useFirstRender.mdx │ │ ├── useInputValue.mdx │ │ ├── useDeviceOS.mdx │ │ ├── useWindowSize.mdx │ │ ├── useSingleEffect.mdx │ │ ├── useScript.mdx │ │ ├── useBatteryStatus.mdx │ │ ├── usePreventBodyScroll.mdx │ │ ├── useIsomorphicLayoutEffect.mdx │ │ ├── useSize.mdx │ │ ├── useUpdateEffect.mdx │ │ ├── useScrollPosition.mdx │ │ ├── useClipboard.mdx │ │ ├── useClickOutside.mdx │ │ ├── useLocalStorage.mdx │ │ ├── useOrientation.mdx │ │ ├── useMediaQuery.mdx │ │ ├── usePrevious.mdx │ │ ├── useConfirmExit.mdx │ │ ├── useKeyPress.mdx │ │ ├── useNetwork.mdx │ │ ├── useScreenSize.mdx │ │ ├── useHold.mdx │ │ ├── useMousePosition.mdx │ │ ├── useDebounce.mdx │ │ ├── useCookieListener.mdx │ │ ├── useInterval.mdx │ │ ├── useToggle.mdx │ │ ├── useCookie.mdx │ │ ├── useEventListener.mdx │ │ ├── useIdle.mdx │ │ ├── useFullscreen.mdx │ │ ├── useTImer.mdx │ │ ├── useIntersectionObserver.mdx │ │ ├── usePermission.mdx │ │ ├── useGeolocation.mdx │ │ └── useTabNotification.mdx │ ├── utilities │ │ ├── _category_.json │ │ ├── renderAfter.mdx │ │ ├── for.mdx │ │ ├── image.mdx │ │ ├── if.mdx │ │ ├── classes.mdx │ │ ├── show.mdx │ │ ├── class.mdx │ │ └── errorBoundary.mdx │ └── intro.mdx ├── .gitignore ├── demo │ ├── RenderAfterDemo.jsx │ ├── ForDemo.jsx │ ├── UseFaviconDemo.jsx │ ├── UseTitleDemo.jsx │ ├── UseHoverDemo.jsx │ ├── UseDeviceOSDemo.jsx │ ├── UseIsomorphicLayoutEffectDemo.jsx │ ├── UseToggleDemo.jsx │ ├── UseClipboardDemo.jsx │ ├── UseSingleEffectDemo.jsx │ ├── UseUrgentUpdateDemo.jsx │ ├── UseKeyPressDemo.jsx │ ├── UseLeaveDetectionDemo.jsx │ ├── UsePrefersThemeDemo.jsx │ ├── UseScriptDemo.jsx │ ├── UseInputValueDemo.jsx │ ├── UseScrollPositionDemo.jsx │ ├── UseMediaQueryDemo.jsx │ ├── UsePreventBodyScrollDemo.jsx │ ├── UseWindowSizeDemo.jsx │ ├── UseHoldDemo.jsx │ ├── UseConfirmExitDemo.jsx │ ├── UseOrientationDemo.tsx │ ├── UseLocalStorageDemo.jsx │ ├── UseBatteryStatusDemo.jsx │ ├── UseClickOutsideDemo.jsx │ ├── UseMousePositionDemo.jsx │ ├── UseFirstRenderDemo.jsx │ ├── IfDemo.jsx │ ├── UseUpdateEffectDemo.jsx │ ├── UseIdleDemo.jsx │ ├── UseFullscreenDemo.jsx │ ├── UseIntersectionObserverDemo.jsx │ ├── ClassDemo.jsx │ ├── UseDebounceDemo.jsx │ ├── UseScreenSizeDemo.jsx │ ├── UseNetworkDemo.tsx │ ├── UseTimerDemo.jsx │ ├── UseEventListenerDemo.jsx │ ├── UsePreviousDemo.tsx │ ├── UseSizeDemo.jsx │ ├── UsePermissionDemo.tsx │ ├── ShowDemo.jsx │ ├── ErrorBoundaryDemo.tsx │ ├── UseIntervalDemo.tsx │ ├── UseGeolocationDemo.jsx │ ├── UseScrollDeviceDemo.tsx │ ├── UseWebSocketDemo.jsx │ ├── UseCookieDemo.tsx │ ├── ClassesDemo.jsx │ ├── SwitchDemo.tsx │ ├── UseCookieListenerDemo.tsx │ └── ImageDemo.tsx ├── sidebars.js ├── LICENSE.md └── package.json ├── .gitignore ├── jest.setup.ts ├── .npmignore ├── .prettierrc ├── lib ├── tests │ ├── utils │ │ ├── If.test.tsx.snap │ │ ├── Classes.test.tsx.snap │ │ ├── For.test.tsx.snap │ │ ├── Switch.test.tsx.snap │ │ ├── If.test.tsx │ │ ├── For.test.tsx │ │ ├── Switch.test.tsx │ │ └── Classes.test.tsx │ └── hooks │ │ ├── useFirstRender.test.tsx │ │ ├── useEventListener.test.tsx │ │ ├── usePrevious.test.tsx │ │ ├── useTitle.test.tsx │ │ ├── useToggle.test.tsx │ │ ├── useNetwork.test.tsx │ │ └── useConfirmExit.test.tsx ├── hooks │ ├── useIsomorphicLayoutEffect.ts │ ├── useUrgentUpdate.ts │ ├── usePrefersTheme.ts │ ├── useFirstRender.ts │ ├── useTitle.ts │ ├── useDebounce.ts │ ├── useUpdateEffect.ts │ ├── useClickOutside.ts │ ├── usePrevious.ts │ ├── useNetwork.ts │ ├── useToggle.ts │ ├── useFavicon.ts │ ├── useLeaveDetection.ts │ ├── useSingleEffect.ts │ ├── useInputValue.ts │ ├── useSize.ts │ ├── useWindowSize.ts │ ├── useHover.ts │ ├── useOrientation.ts │ ├── useEventListener.ts │ ├── useScrollPosition.ts │ ├── useConfirmExit.ts │ ├── useCookie.ts │ ├── usePreventBodyScroll.ts │ ├── useDeviceOS.ts │ ├── useClipboard.ts │ ├── useMousePosition.ts │ ├── useIdle.ts │ ├── useMediaQuery.ts │ ├── useCookieListener.ts │ ├── useKeyPress.ts │ ├── useIntersectionObserver.ts │ ├── useScript.ts │ ├── useHold.ts │ ├── useLocalStorage.ts │ ├── useScrollDevice.ts │ ├── useScreenSize.ts │ ├── useBatteryStatus.ts │ └── useMutation.ts ├── utils │ ├── If.tsx │ ├── For.tsx │ ├── RenderAfter.tsx │ ├── Image.tsx │ ├── Show.tsx │ ├── Switch.tsx │ ├── Class.tsx │ ├── ErrorBoundary.tsx │ └── Classes.tsx └── helpers │ ├── event.ts │ └── cookie.ts ├── .editorconfig ├── .github └── ISSUE_TEMPLATE │ └── feature-request.md ├── jest.config.ts ├── snapshotResolver.ts ├── tsconfig.json ├── LICENSE.md └── package.json /docs/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | coverage -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !README.md 3 | !LICENSE 4 | !dist/** 5 | !package.json 6 | !package-lock.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "printWidth": 80, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/docs/hooks/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Hooks", 3 | "collapsible": false, 4 | "position": 4, 5 | "link": { 6 | "type": "generated-index" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/docs/utilities/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Utilities", 3 | "collapsible": false, 4 | "position": 3, 5 | "link": { 6 | "type": "generated-index" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/tests/utils/If.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`If renders the If component 1`] = ` 4 | 5 | If Block 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /lib/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | export const useIsomorphicLayoutEffect = 4 | typeof window !== 'undefined' ? useLayoutEffect : useEffect; 5 | -------------------------------------------------------------------------------- /lib/hooks/useUrgentUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | const r = (v: number) => (v + 1) % 1000000; 3 | 4 | export function useUrgentUpdate() { 5 | const [_, u] = useReducer(r, 0); 6 | 7 | return u; 8 | } 9 | -------------------------------------------------------------------------------- /lib/utils/If.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type Props = { 4 | isTrue: boolean; 5 | children: ReactNode; 6 | }; 7 | 8 | export const If = ({ isTrue, children }: Props) => (isTrue ? children : null); 9 | -------------------------------------------------------------------------------- /lib/hooks/usePrefersTheme.ts: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from './useMediaQuery'; 2 | 3 | export function usePrefersTheme(initialValue: 'light' | 'dark') { 4 | return useMediaQuery('(prefers-color-scheme: dark)', initialValue === 'dark') 5 | ? 'dark' 6 | : 'light'; 7 | } 8 | -------------------------------------------------------------------------------- /lib/hooks/useFirstRender.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export function useFirstRender() { 4 | const isFirst = useRef(true); 5 | 6 | if (isFirst.current) { 7 | isFirst.current = false; 8 | return true; 9 | } 10 | 11 | return isFirst.current; 12 | } 13 | -------------------------------------------------------------------------------- /lib/tests/utils/Classes.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Classes component renders the Classes component 1`] = ` 4 | 5 |
8 | Classes Block 9 |
10 |
11 | `; 12 | -------------------------------------------------------------------------------- /lib/tests/utils/For.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For renders the For component 1`] = ` 4 | 5 | 6 | 0: foo 7 | 8 | 9 | 1: bar 10 | 11 | 12 | 2: baz 13 | 14 | 15 | `; 16 | -------------------------------------------------------------------------------- /lib/hooks/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 2 | 3 | export function useTitle(title: string) { 4 | useIsomorphicLayoutEffect(() => { 5 | if (typeof title === 'string' && title.trim().length > 0) { 6 | document.title = title.trim(); 7 | } 8 | }, [title]); 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/For.tsx: -------------------------------------------------------------------------------- 1 | import {Children, type ReactNode} from 'react'; 2 | 3 | type RenderFn = (item: T, index?: number) => ReactNode; 4 | 5 | export const For = ({ 6 | render, 7 | each = [], 8 | }: { 9 | render: RenderFn; 10 | each?: T[]; 11 | }) => Children.toArray(each.map((item, index) => render(item, index))); 12 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature for this project 4 | title: "[FEAT] Add [...]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description** 11 | A clear and concise description of what the problem is. 12 | 13 | **Acceptance Criteria** 14 | - [ ] Condition 1 15 | - [ ] Condition 2 16 | - [ ] ... 17 | -------------------------------------------------------------------------------- /docs/demo/RenderAfterDemo.jsx: -------------------------------------------------------------------------------- 1 | import { RenderAfter } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const RenderAfterDemo = () => { 5 | return ( 6 |
7 | 8 | Wait 5 seconds and I'll show up! 9 | 10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /lib/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useDebounce(value: T, delay = 500) { 4 | const [debounced, setDebounced] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => setDebounced(value), delay); 8 | 9 | return () => clearTimeout(timer); 10 | }, [value, delay]); 11 | 12 | return debounced; 13 | } 14 | -------------------------------------------------------------------------------- /docs/demo/ForDemo.jsx: -------------------------------------------------------------------------------- 1 | import { For } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const ForDemo = () => { 5 | const data = [{name: 'React'}, {name: 'Haiku'}]; 6 | 7 | return( 8 |
9 | 10 | {`${index}: ${item.name}`} 11 | }/> 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /lib/hooks/useUpdateEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { useFirstRender } from './useFirstRender'; 4 | 5 | export function useUpdateEffect( 6 | effect: () => void | (() => void | undefined), 7 | deps: ReadonlyArray, 8 | ) { 9 | const isFirstRender = useFirstRender(); 10 | 11 | useEffect(() => { 12 | if (!isFirstRender) { 13 | return effect(); 14 | } 15 | }, deps); 16 | } 17 | -------------------------------------------------------------------------------- /docs/demo/UseFaviconDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useFavicon } from "react-haiku"; 3 | import React from 'react'; 4 | 5 | export const UseFaviconDemo = () => { 6 | const { setFavicon } = useFavicon(); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /docs/demo/UseTitleDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useTitle } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseTitleDemo = () => { 5 | const [title, setTitle] = React.useState(''); 6 | useTitle(title); 7 | 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /lib/helpers/event.ts: -------------------------------------------------------------------------------- 1 | export const on = ( 2 | obj: T, 3 | ...args: A 4 | ) => { 5 | // @ts-ignore 6 | if (obj && obj.addEventListener) obj.addEventListener(...args); 7 | }; 8 | 9 | export const off = ( 10 | obj: T, 11 | ...args: A 12 | ) => { 13 | // @ts-ignore 14 | if (obj && obj.removeEventListener) obj.removeEventListener(...args); 15 | }; 16 | -------------------------------------------------------------------------------- /docs/demo/UseHoverDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useHover } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseHoverDemo = () => { 5 | const { hovered, ref } = useHover(); 6 | 7 | return( 8 |
9 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /lib/hooks/useClickOutside.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useEventListener } from './useEventListener'; 3 | 4 | export function useClickOutside( 5 | ref: React.RefObject, 6 | handler: (e: Event) => any, 7 | event = 'mousedown', 8 | ) { 9 | useEventListener(event, (event) => { 10 | const el = ref?.current; 11 | 12 | if (!el || el.contains(event.target)) { 13 | return; 14 | } 15 | 16 | handler(event); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Layout from '@theme/Layout'; 3 | import Homepage from '@site/src/components/Homepage'; 4 | 5 | export default function Home() { 6 | return ( 7 | 10 | 11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/components/Demo/LeaveDetection.js: -------------------------------------------------------------------------------- 1 | import { useLeaveDetection } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const LeaveDetection = () => { 5 | const [leaveCount, setLeaveCount] = React.useState(0); 6 | useLeaveDetection(() => setLeaveCount((s) => s + 1)); 7 | 8 | return ( 9 |
10 | Your mouse left the viewport {leaveCount} time{leaveCount === 1 ? '' : 's'}! 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /docs/demo/UseDeviceOSDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useDeviceOS } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseDeviceOSDemo = () => { 5 | const deviceOS = useDeviceOS(); 6 | 7 | return ( 8 |
9 | Check Your Device OS! 10 |

Operating System: {deviceOS}

11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /docs/demo/UseIsomorphicLayoutEffectDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useIsomorphicLayoutEffect } from "react-haiku"; 2 | import React from 'react'; 3 | 4 | export const UseIsomorphicLayoutEffectDemo = () => { 5 | useIsomorphicLayoutEffect(() => { 6 | // do whatever 7 | }, []) 8 | 9 | return ( 10 |
11 | SSR will run useEffect 12 | Browser will run useLayoutEffect 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /docs/demo/UseToggleDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useToggle } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseToggleDemo = () => { 5 | const [theme, toggleTheme] = useToggle('dark', ['dark', 'light']); 6 | 7 | return ( 8 |
9 | {`Theme: ${theme}`} 10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /lib/tests/utils/Switch.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Switch Component should render the correct component based on value 1`] = ` 4 | 5 |
6 | Component A 7 |
8 |
9 | `; 10 | 11 | exports[`Switch Component should render the default component if value does not match any case 1`] = ` 12 | 13 |
14 | Default Component 15 |
16 |
17 | `; 18 | -------------------------------------------------------------------------------- /lib/utils/RenderAfter.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode, useEffect, useState } from 'react'; 2 | 3 | type Props = { 4 | delay?: number; 5 | children: ReactNode; 6 | }; 7 | 8 | export function RenderAfter({ delay = 1000, children }: Props) { 9 | const [ready, setReady] = useState(false); 10 | 11 | useEffect(() => { 12 | const timer = setTimeout(() => setReady(true), delay); 13 | return () => clearTimeout(timer); 14 | }, [ready]); 15 | 16 | return ready ? children : null; 17 | } 18 | -------------------------------------------------------------------------------- /docs/demo/UseClipboardDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useClipboard } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseClipboardDemo = () => { 5 | const clipboard = useClipboard({ timeout: 2000 }); 6 | 7 | return( 8 |
9 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /lib/utils/Image.tsx: -------------------------------------------------------------------------------- 1 | import { ImgHTMLAttributes } from 'react'; 2 | 3 | type Props = ImgHTMLAttributes & { fallback: string }; 4 | 5 | export const Image = ({ src, loading, alt, fallback, ...props }: Props) => { 6 | const handleBrokenImage = (event: any) => (event.target.src = fallback); 7 | 8 | return ( 9 | {alt} 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/demo/UseSingleEffectDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useSingleEffect } from "react-haiku"; 2 | import React from 'react'; 3 | 4 | export const UseSingleEffectDemo = () => { 5 | const [renderCount, setRenderCount] = React.useState(0); 6 | 7 | useSingleEffect(() => { 8 | setRenderCount(renderCount + 1); 9 | }) // no dependency array needed 10 | 11 | return ( 12 |
13 | Effect executed only {renderCount} time! 14 |
15 | ); 16 | } -------------------------------------------------------------------------------- /docs/demo/UseUrgentUpdateDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useUrgentUpdate } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseUrgentUpdateDemo = () => { 5 | const update = useUrgentUpdate(); 6 | const randomNo = Math.floor(Math.random() * 100); 7 | 8 | return ( 9 |
10 | {`Number: ${randomNo}`} 11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /docs/demo/UseKeyPressDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useKeyPress } from 'react-haiku'; 2 | import React, { useState } from 'react'; 3 | 4 | export const UseKeyPressDemo = () => { 5 | const [didKeyPress, setDidKeyPress] = useState(false); 6 | useKeyPress(['Control', 'Shift', 'A'], (e) => { 7 | setDidKeyPress(true); 8 | }); 9 | 10 | return ( 11 |
12 |

Press Control + Shift + A

13 | {didKeyPress &&

{`You pressed : Control + Shift + A`}

} 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /docs/demo/UseLeaveDetectionDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useLeaveDetection } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseLeaveDetectionDemo = () => { 5 | const [leaveCount, setLeaveCount] = React.useState(0); 6 | useLeaveDetection(() => setLeaveCount((s) => s + 1)); 7 | 8 | return ( 9 |
10 | 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /docs/demo/UsePrefersThemeDemo.jsx: -------------------------------------------------------------------------------- 1 | import { usePrefersTheme } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UsePrefersThemeDemo = () => { 5 | const theme = usePrefersTheme(); 6 | 7 | return ( 8 |
9 | 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /docs/src/components/Demo/ClickOutside.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useClickOutside } from "react-haiku" 3 | 4 | export const ClickOutside = () => { 5 | const [count, setCount] = React.useState(0); 6 | const ref = React.useRef(null) 7 | 8 | const handleClickOutside = () => setCount(count + 1); 9 | 10 | useClickOutside(ref, handleClickOutside); 11 | 12 | return ( 13 |
14 | Clicked outside of this slide {count} time{count === 1 ? '' : 's'}! 15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /docs/demo/UseScriptDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useScript } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseScriptDemo = () => { 5 | const script = useScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js'); 6 | 7 | return ( 8 |
9 | Check the bottom of the body tag! 10 | 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /docs/demo/UseInputValueDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useInputValue } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseInputValueDemo = () => { 5 | const [nameValue, setNameValue] = useInputValue(''); 6 | 7 | return ( 8 |
9 | 14 |

{`Value - ${nameValue ? nameValue : 'None'}`}

15 |
16 | ); 17 | } -------------------------------------------------------------------------------- /docs/demo/UseScrollPositionDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useScrollPosition } from "react-haiku"; 2 | import React from 'react'; 3 | 4 | export const UseScrollPositionDemo = () => { 5 | const [scroll, setScroll] = useScrollPosition(); 6 | 7 | return ( 8 |
9 | Current Position: {`X: ${scroll.x}, Y: ${scroll.y}`}! 10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /docs/static/img/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 4 | 5 | /** 6 | * Tracks the previous value of a given input. 7 | * 8 | * @param {T} value The current value to track. 9 | * @returns {T} The previous value of the input, or `value` if it's the first render. 10 | */ 11 | export const usePrevious = (value: T): T => { 12 | const ref = useRef(value) 13 | 14 | useIsomorphicLayoutEffect(() => { 15 | ref.current = value 16 | }, [value]) 17 | 18 | return ref.current 19 | } 20 | -------------------------------------------------------------------------------- /docs/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/hooks/useNetwork.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { useEventListener } from './useEventListener'; 3 | 4 | export function useNetwork() { 5 | const [isOnline, setIsOnline] = useState(navigator?.onLine || true); 6 | 7 | const handleOnline = useCallback(() => { 8 | setIsOnline(true); 9 | }, []); 10 | 11 | const handleOffline = useCallback(() => { 12 | setIsOnline(false); 13 | }, []); 14 | 15 | useEventListener('online', handleOnline); 16 | useEventListener('offline', handleOffline); 17 | 18 | return isOnline; 19 | } 20 | -------------------------------------------------------------------------------- /lib/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useToggle(initialValue: T, options: [T, T]) { 4 | const [state, setState] = useState(initialValue); 5 | const handleToggle = () => 6 | setState((current) => (current === options[0] ? options[1] : options[0])); 7 | 8 | const toggle = (value: T) => 9 | typeof value !== 'undefined' ? setState(value) : handleToggle(); 10 | 11 | return [state, toggle]; 12 | } 13 | 14 | export function useBoolToggle(initialValue = false) { 15 | return useToggle(initialValue, [true, false]); 16 | } 17 | -------------------------------------------------------------------------------- /docs/demo/UseMediaQueryDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseMediaQueryDemo = () => { 5 | const breakpoint = useMediaQuery('(max-width: 1200px)'); 6 | 7 | return ( 8 |
9 |

Resize your window!

10 | 13 |
14 | ); 15 | } -------------------------------------------------------------------------------- /docs/demo/UsePreventBodyScrollDemo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { usePreventBodyScroll } from 'react-haiku'; 3 | 4 | export const UsePreventBodyScrollDemo = () => { 5 | const { isScrollLocked, toggleScrollLock } = usePreventBodyScroll(); 6 | 7 | return ( 8 |
9 | 10 | Scroll: {isScrollLocked ? 'Disabled' : 'Enabled'} 11 | 12 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/demo/UseWindowSizeDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseWindowSizeDemo = () => { 5 | const {height, width} = useWindowSize(); 6 | return ( 7 |
8 | Resize Your Window! 9 |

Window Height: {height}

10 |

Window Width: {width}

11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /lib/hooks/useFavicon.ts: -------------------------------------------------------------------------------- 1 | export function useFavicon(defaultHref: string | null = null) { 2 | const set = (hrefToSet: string) => { 3 | const link: HTMLLinkElement | null = 4 | document.querySelector("link[rel*='icon']") || 5 | document.createElement('link'); 6 | 7 | link.type = 'image/x-icon'; 8 | link.rel = 'shortcut icon'; 9 | link.href = hrefToSet; 10 | 11 | document.getElementsByTagName('head')[0]?.appendChild(link); 12 | }; 13 | 14 | const setFavicon = (href: string) => 15 | defaultHref && !href ? set(defaultHref) : set(href); 16 | 17 | return { setFavicon }; 18 | } 19 | -------------------------------------------------------------------------------- /docs/demo/UseHoldDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useHold } from "react-haiku" 3 | import React from 'react'; 4 | 5 | export const UseHoldDemo = () => { 6 | const [count, setCount] = React.useState(0) 7 | const handleHold = () => setCount(count + 1); 8 | 9 | const buttonHold = useHold(handleHold, { delay: 2000 }); 10 | 11 | return ( 12 |
13 | Successful Holds: {count} 14 | 17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /docs/demo/UseConfirmExitDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useConfirmExit, useBoolToggle } from "react-haiku" 3 | import React from 'react'; 4 | 5 | export const UseConfirmExitDemo = () => { 6 | const [dirty, toggleDirty] = useBoolToggle(); 7 | useConfirmExit(dirty); 8 | 9 | return ( 10 |
11 | Try to close this tab with the window dirty! 12 |

Dirty: {`${dirty}`}

13 | 14 |
15 | ); 16 | } -------------------------------------------------------------------------------- /docs/demo/UseOrientationDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useOrientation } from 'react-haiku'; 3 | 4 | export const UseOrientationDemo = () => { 5 | const orientation = useOrientation(); 6 | 7 | return ( 8 |
9 |

Current Orientation:

10 |
18 | {orientation.toUpperCase()} 19 |
20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /docs/demo/UseLocalStorageDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useLocalStorage } from "react-haiku"; 3 | import React from 'react'; 4 | 5 | export const UseLocalStorageDemo = () => { 6 | const [value, setValue] = useLocalStorage('message'); 7 | 8 | React.useEffect(() => { 9 | setValue({ message: 'Hello!' }) 10 | }, []) 11 | 12 | return ( 13 |
14 | Storage Value: {value?.message} 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /docs/demo/UseBatteryStatusDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useBatteryStatus } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseBatteryStatusDemo = () => { 5 | const {level, isCharging} = useBatteryStatus(); 6 | 7 | return ( 8 |
9 | Battery Status! 10 |

Battery Level: {level}

11 |

Is Battery Charging: {isCharging ? "True" : "False"}

12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /docs/docs/hooks/useHover.mdx: -------------------------------------------------------------------------------- 1 | # useHover() 2 | 3 | The useHover() hook lets you detect if the user's mouse is hovering over an element 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useHover } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseHoverDemo } from '../../demo/UseHoverDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useHover } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const { hovered, ref } = useHover(); 22 | 23 | return( 24 | 27 | ); 28 | } 29 | ``` -------------------------------------------------------------------------------- /docs/demo/UseClickOutsideDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useClickOutside } from "react-haiku"; 3 | import React from 'react'; 4 | 5 | export const UseClickOutsideDemo = () => { 6 | const [count, setCount] = React.useState(0); 7 | const ref = React.useRef(null) 8 | 9 | const handleClickOutside = () => setCount(count + 1); 10 | 11 | useClickOutside(ref, handleClickOutside); 12 | 13 | return ( 14 |
15 | Clicked Outside {count} Times! 16 | 17 |
18 | ); 19 | } -------------------------------------------------------------------------------- /docs/docs/utilities/renderAfter.mdx: -------------------------------------------------------------------------------- 1 | # RenderAfter 2 | 3 | The `RenderAfter` component can be used to render components or JSX code wrapped inside of it after a set delay. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { RenderAfter } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { RenderAfterDemo } from '../../demo/RenderAfterDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { RenderAfter } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | return( 22 | 23 | Wait 5 seconds and I'll show up! 24 | 25 | ); 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /lib/tests/utils/If.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { If } from '../../utils/If'; 3 | 4 | describe('If', () => { 5 | const text = 'If Block'; 6 | 7 | it('renders the If component', () => { 8 | const { asFragment } = render({text}); 9 | expect(asFragment()).toMatchSnapshot(); 10 | expect(screen.getByText(text)).toBeInTheDocument(); 11 | }); 12 | 13 | it('should not render anything if isTrue is false', () => { 14 | render({text}); 15 | expect(screen.queryByText(text)).toBeNull(); 16 | expect(screen.queryByText(text)).not.toBeInTheDocument(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /docs/docs/hooks/useFavicon.mdx: -------------------------------------------------------------------------------- 1 | # useFavicon() 2 | 3 | The `useFavicon()` hook lets change the website's favicon dynamically from your components! The favicon changes back to the default one on refresh! 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useFavicon } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseFaviconDemo } from '../../demo/UseFaviconDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useFavicon } from "react-haiku" 19 | 20 | export const Component = () => { 21 | const { setFavicon } = useFavicon(); 22 | 23 | return 24 | } 25 | ``` -------------------------------------------------------------------------------- /lib/hooks/useLeaveDetection.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useLeaveDetection( 4 | onLeave: (this: HTMLElement, ev: MouseEvent) => any, 5 | ) { 6 | const onLeaveRef = useRef(onLeave); 7 | 8 | useEffect(() => { 9 | onLeaveRef.current = onLeave; 10 | }, [onLeave]); 11 | 12 | useEffect(() => { 13 | const handler = function (this: HTMLElement, ev: MouseEvent) { 14 | onLeaveRef.current.call(this, ev); 15 | }; 16 | 17 | document.documentElement.addEventListener('mouseleave', handler); 18 | 19 | return () => 20 | document.documentElement.removeEventListener('mouseleave', handler); 21 | }, []); 22 | } 23 | -------------------------------------------------------------------------------- /docs/demo/UseMousePositionDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useMousePosition } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseMousePositionDemo = () => { 5 | const { target, x, y } = useMousePosition(); 6 | 7 | return ( 8 |
9 | Hover This Container 10 |

{`X: ${x} | Y: ${y}`}

11 |
12 | ); 13 | } 14 | 15 | export const UseMousePositionFullDemo = () => { 16 | const { x, y } = useMousePosition(); 17 | 18 | return ( 19 |
20 |

{`X: ${x} | Y: ${y}`}

21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /docs/demo/UseFirstRenderDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | /* The functionality in this demo had to be emulated due to Docusaurus re-rendering 5 | my Demo component multiple times and not allowing the showcase of the hook */ 6 | 7 | export const UseFirstRenderDemo = () => { 8 | const [isFirst, setFirst] = React.useState(true); 9 | 10 | return ( 11 |
12 | First Render? - {isFirst ? 'Yes' : 'No'} 13 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /lib/hooks/useSingleEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useSingleEffect(effect: () => void | (() => void)) { 4 | const destroy = useRef void)>(undefined); 5 | const calledOnce = useRef(false); 6 | const renderAfterCalled = useRef(false); 7 | 8 | if (calledOnce.current) renderAfterCalled.current = true; 9 | 10 | useEffect(() => { 11 | if (calledOnce.current) { 12 | return; 13 | } 14 | 15 | calledOnce.current = true; 16 | destroy.current = effect(); 17 | 18 | return () => { 19 | if (!renderAfterCalled.current) { 20 | return; 21 | } 22 | if (destroy.current) destroy.current(); 23 | }; 24 | }, [effect]); 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/components/Demo/Debounce.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useDebounce } from "react-haiku" 3 | 4 | export const Debounce = () => { 5 | const [value, setValue] = React.useState('') 6 | const debouncedValue = useDebounce(value, 1000) 7 | 8 | const handleChange = (event) => setValue(event.target.value) 9 | 10 | return ( 11 |
12 |
13 | Realtime Value:{value}
14 | Debounce value:{debouncedValue} 15 |
16 | 17 | 18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /docs/demo/IfDemo.jsx: -------------------------------------------------------------------------------- 1 | import { If } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const IfDemo = () => { 5 | const [number, setNumber] = React.useState(6); 6 | 7 | return( 8 |
9 | Click to update state! 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /docs/demo/UseUpdateEffectDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useUpdateEffect } from "react-haiku"; 2 | import React from 'react'; 3 | 4 | export const UseUpdateEffectDemo = () => { 5 | const [count, setCount] = React.useState(0); 6 | const [triggerCount, setTriggerCount] = React.useState(0); 7 | 8 | useUpdateEffect(() => { 9 | setTriggerCount(triggerCount + 1); 10 | }, [count]) // no dependency array needed 11 | 12 | return ( 13 |
14 | Updates Detected: {triggerCount} 15 | 16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /lib/tests/hooks/useFirstRender.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useFirstRender } from '../../hooks/useFirstRender'; 3 | 4 | describe('useFirstRender', () => { 5 | it('should return true on first render', () => { 6 | const { result } = renderHook(() => useFirstRender()); 7 | 8 | expect(result.current).toBe(true); 9 | }); 10 | 11 | it('should maintain false state across multiple rerenders', () => { 12 | const { result, rerender } = renderHook(() => useFirstRender()); 13 | 14 | // Multiple subsequent renders. 15 | for (let i = 0; i < 5; i++) { 16 | rerender(); 17 | 18 | expect(result.current).toBe(false); 19 | } 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /docs/demo/UseIdleDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useIdle } from "react-haiku"; 2 | import React from 'react'; 3 | 4 | export const UseIdleDemo = () => { 5 | const idle = useIdle(3000); 6 | 7 | return ( 8 |
9 | Current Status: {idle ? 'Idle' : 'Active'} 10 |
11 | ); 12 | } 13 | 14 | export const UseIdleCustomDemo = () => { 15 | const idle = useIdle(1000, { events: ['click', 'touchstart'], initialState: false }); 16 | 17 | return ( 18 |
19 | Works only with click/touch events! 20 | Current Status: {idle ? 'Idle' : 'Active'} 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /docs/demo/UseFullscreenDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useFullscreen } from 'react-haiku' 2 | import React from 'react' 3 | 4 | export const UseFullscreenDemo = () => { 5 | const documentRef = React.useRef(null); 6 | 7 | React.useEffect(() => { 8 | documentRef.current = document.documentElement; 9 | }, []); 10 | 11 | const {isFullscreen, toggleFullscreen } = useFullscreen(documentRef); 12 | return ( 13 |
14 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"} 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/tests/utils/For.test.tsx: -------------------------------------------------------------------------------- 1 | import { For } from '../../utils/For'; 2 | import { render, screen } from '@testing-library/react'; 3 | 4 | describe('For', () => { 5 | const testArray = ['foo', 'bar', 'baz']; 6 | 7 | it('renders the For component', () => { 8 | const { asFragment } = render( 9 | {`${index}: ${item}`}} 12 | />, 13 | ); 14 | expect(asFragment()).toMatchSnapshot(); 15 | 16 | testArray.forEach((item, index) => { 17 | expect(screen.getByText(`${index}: ${item}`)).toBeInTheDocument(); 18 | }); 19 | expect(document.body.firstChild!.childNodes).toHaveLength(testArray.length); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /docs/demo/UseIntersectionObserverDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useIntersectionObserver } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseIntersectionObserverDemo = () => { 5 | const {observeRef, isVisible} = useIntersectionObserver({ 6 | animateOnce: false, 7 | options:{ 8 | threshold: .5, 9 | rootMargin: '-40% 0px -40% 0px' 10 | } 11 | }) 12 | return ( 13 |
14 | Scroll Your Window! 15 |

16 | We {isVisible? 'are': 'are not'} intersecting! 17 |

18 |
19 | ); 20 | } -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | 3 | const config: Config = { 4 | collectCoverage: true, 5 | collectCoverageFrom: ["lib/**/*.{ts,tsx}", "!lib/**/*.d.ts", "!**/vendor/**"], 6 | coverageDirectory: "coverage", 7 | testEnvironment: "jsdom", 8 | testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"], 9 | transform: { 10 | ".(ts|tsx)": "ts-jest", 11 | }, 12 | 13 | coveragePathIgnorePatterns: [ 14 | "/node_modules/", 15 | "/coverage", 16 | "package.json", 17 | "package-lock.json", 18 | "reportWebVitals.ts", 19 | "jest.setup.ts", 20 | "index.tsx", 21 | ], 22 | setupFilesAfterEnv: ["/jest.setup.ts"], 23 | snapshotResolver: "/snapshotResolver.ts", 24 | }; 25 | 26 | export default config; 27 | -------------------------------------------------------------------------------- /snapshotResolver.ts: -------------------------------------------------------------------------------- 1 | import type { SnapshotResolver } from "jest-snapshot"; 2 | 3 | const snapshotResolver: SnapshotResolver = { 4 | resolveSnapshotPath: ( 5 | testPath: string, 6 | snapshotExtension: string 7 | ): string => { 8 | // Stores the snapshot next to the test file, replacing `.test.tsx` with `.test.tsx.snap` 9 | return testPath + snapshotExtension; 10 | }, 11 | resolveTestPath: ( 12 | snapshotFilePath: string, 13 | snapshotExtension: string 14 | ): string => { 15 | // Reverts the snapshot file path back to the test file path 16 | return snapshotFilePath.slice(0, -snapshotExtension.length); 17 | }, 18 | testPathForConsistencyCheck: "some/example.test.tsx", 19 | }; 20 | 21 | export default snapshotResolver; 22 | -------------------------------------------------------------------------------- /docs/demo/ClassDemo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Class } from 'react-haiku'; 3 | 4 | export const ClassDemo = () => { 5 | const [isActive, setIsActive] = React.useState(false); 6 | 7 | return ( 8 |
9 | 16 | 22 | This is a Class component 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /docs/src/components/Demo/Hold.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHold } from "react-haiku" 3 | 4 | export const Hold = () => { 5 | const [count, setCount] = React.useState(0) 6 | const [holdText, setHoldText] = React.useState('Press & Hold!'); 7 | const handleHold = () => { 8 | setCount(count + 1); 9 | setHoldText('Let go!'); 10 | 11 | setTimeout(() => { 12 | setHoldText('Press & Hold!') 13 | }, 1000) 14 | } 15 | 16 | const buttonHold = useHold(handleHold, { delay: 2000 }); 17 | 18 | return ( 19 |
20 | Successfully held{count}time{count === 1 ? '' : 's'}!
21 | 24 |
25 | ); 26 | } -------------------------------------------------------------------------------- /docs/demo/UseDebounceDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useDebounce } from "react-haiku" 3 | import React from 'react'; 4 | 5 | export const UseDebounceDemo = () => { 6 | const [value, setValue] = React.useState('') 7 | const debouncedValue = useDebounce(value, 1000) 8 | 9 | const handleChange = (event) => setValue(event.target.value) 10 | 11 | React.useEffect(() => { 12 | console.log(debouncedValue); 13 | }, [debouncedValue]) 14 | 15 | return ( 16 |
17 | Real-Time Value: {value} 18 | Debounced value: {debouncedValue} 19 | 20 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /docs/docs/hooks/usePrefersTheme.mdx: -------------------------------------------------------------------------------- 1 | # usePrefersTheme() 2 | 3 | The `usePrefersTheme()` hook allows the detection of the user's preferred system theme 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { usePrefersTheme } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UsePrefersThemeDemo } from '../../demo/UsePrefersThemeDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { usePrefersTheme } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const theme = usePrefersTheme(); 22 | 23 | return ( 24 | 30 | ); 31 | } 32 | ``` -------------------------------------------------------------------------------- /lib/hooks/useInputValue.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function handleChange(setValue: React.Dispatch) { 4 | return (val: T) => { 5 | if (!val) { 6 | setValue(val); 7 | } else if (typeof val === 'object' && 'nativeEvent' in val) { 8 | // @ts-ignore 9 | const { currentTarget } = val; 10 | 11 | if (currentTarget.type === 'checkbox') { 12 | setValue(currentTarget.checked); 13 | } else { 14 | setValue(currentTarget.value); 15 | } 16 | } else { 17 | setValue(val); 18 | } 19 | }; 20 | } 21 | 22 | export function useInputValue(initialState: T) { 23 | const [value, setValue] = useState(initialState); 24 | 25 | return [value, handleChange(setValue)] as const; 26 | } 27 | -------------------------------------------------------------------------------- /lib/tests/hooks/useEventListener.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useEventListener } from '../../hooks/useEventListener'; 3 | 4 | describe('useEventListener Hook', () => { 5 | test('adds and triggers event listener correctly', () => { 6 | const handler = jest.fn(); 7 | 8 | const { unmount } = renderHook(() => useEventListener('click', handler)); 9 | 10 | act(() => { 11 | window.dispatchEvent(new MouseEvent('click')); 12 | }); 13 | 14 | expect(handler).toHaveBeenCalledTimes(1); 15 | 16 | unmount(); 17 | 18 | act(() => { 19 | window.dispatchEvent(new MouseEvent('click')); 20 | }); 21 | 22 | expect(handler).toHaveBeenCalledTimes(1); // Should not trigger after unmount 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /docs/docs/hooks/useTitle.mdx: -------------------------------------------------------------------------------- 1 | # useTitle() 2 | 3 | The `useTitle()` hook allows the dynamic update of the document's title from your React components! The title passed to this hook can be attached to a piece of state, updating the state will therefore also update the title. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useTitle } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseTitleDemo } from '../../demo/UseTitleDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { useTitle } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [title, setTitle] = useState(''); 23 | useTitle(title); 24 | 25 | return 26 | } 27 | ``` -------------------------------------------------------------------------------- /lib/hooks/useSize.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useSize = (ref: any) => { 4 | const [dimensions, setDimensions] = useState({ 5 | width: 0, 6 | height: 0, 7 | }); 8 | 9 | useEffect(() => { 10 | if (!ref.current) { 11 | return; 12 | } 13 | 14 | const updateDimensions = () => { 15 | const { clientWidth, clientHeight } = ref.current; 16 | setDimensions({ width: clientWidth, height: clientHeight }); 17 | }; 18 | 19 | const resizeObserver = new ResizeObserver(updateDimensions); 20 | 21 | updateDimensions(); 22 | 23 | resizeObserver.observe(ref.current); 24 | 25 | return () => { 26 | resizeObserver.disconnect(); 27 | }; 28 | }, [ref.current]); 29 | 30 | return dimensions; 31 | }; 32 | -------------------------------------------------------------------------------- /docs/docs/hooks/useUrgentUpdate.mdx: -------------------------------------------------------------------------------- 1 | # useUrgentUpdate() 2 | 3 | The `useUrgentUpdate()` hook forces a component to re-render when it gets called from anywhere inside it. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useUrgentUpdate } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseUrgentUpdateDemo } from '../../demo/UseUrgentUpdateDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useUrgentUpdate } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const randomNo = Math.floor(Math.random() * 100); 22 | const update = useUrgentUpdate(); 23 | 24 | return ( 25 | <> 26 | {`Number: ${randomNo}`} 27 | 28 | 29 | ); 30 | } 31 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/useScrollDevice.mdx: -------------------------------------------------------------------------------- 1 | # useScrollDevice() 2 | 3 | The useScrollDevice() hook detects whether the user is scrolling with a mouse wheel or trackpad. Use this to adapt scroll behaviors or animations based on input device. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useScrollDevice } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseScrollDeviceDemo } from '../../demo/UseScrollDeviceDemo.tsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useScrollDevice } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const device = useScrollDevice(); 22 | 23 | return

Detected scroll device: {device || 'unknown'}

; 24 | }; 25 | ``` 26 | 27 | ### API 28 | 29 | #### Returns 30 | 31 | - `scrollDevice` - Detected device type: "mouse", "trackpad", or null 32 | -------------------------------------------------------------------------------- /docs/static/img/copy-success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/hooks/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | type WindowSizeProps = { 4 | width: number; 5 | height: number; 6 | }; 7 | 8 | export const useWindowSize = (): WindowSizeProps => { 9 | const [windowSize, setWindowSize] = useState({ 10 | width: window?.innerWidth || 0, 11 | height: window?.innerHeight || 0 12 | }); 13 | 14 | useEffect(() => { 15 | const handleResize = () => { 16 | if (typeof window !== "undefined") { 17 | setWindowSize({ 18 | width: window.innerWidth, 19 | height: window.innerHeight 20 | }) 21 | } 22 | } 23 | 24 | window.addEventListener("resize", handleResize); 25 | return () => window.removeEventListener("resize", handleResize); 26 | }, []) 27 | 28 | return windowSize; 29 | } -------------------------------------------------------------------------------- /docs/demo/UseScreenSizeDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useScreenSize } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | 5 | export const UseScreenSizeDemo = () => { 6 | const screenSize = useScreenSize(); 7 | 8 | return ( 9 |
10 |

Current Screen Size: {screenSize.toString()}

11 |

Is the screen size medium ? {screenSize.eq("md") ? "Yes" : "No"}

12 |

Is the screen size less than large ? {screenSize.lt("lg") ? "Yes" : "No"}

13 |

Is the screen size greater than small ? {screenSize.gt("sm") ? "Yes" : "No"}

14 |

Is the screen size greater than or equal to small ? {screenSize.gte("sm") ? "Yes" : "No"}

15 |

Is the screen size less than or equal to small ? {screenSize.lte("sm") ? "Yes" : "No"}

16 |
17 | ); 18 | } -------------------------------------------------------------------------------- /docs/docs/hooks/useLeaveDetection.mdx: -------------------------------------------------------------------------------- 1 | # useLeaveDetection() 2 | 3 | The useLeaveDetection() hook allows you to detect when a user's cursor leaves the document's boundaries 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useLeaveDetection } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseLeaveDetectionDemo } from '../../demo/UseLeaveDetectionDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useLeaveDetection } from 'react-haiku'; 19 | import { useState } from 'react'; 20 | 21 | export const Component = () => { 22 | const [leaveCount, setLeaveCount] = useState(0); 23 | useLeaveDetection(() => setLeaveCount((s) => s + 1)); 24 | 25 | return ( 26 | 29 | ); 30 | } 31 | ``` -------------------------------------------------------------------------------- /lib/hooks/useHover.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef, useCallback } from 'react'; 2 | 3 | export function useHover() { 4 | const [hovered, setHovered] = useState(false); 5 | const ref = useRef(null); 6 | const onMouseEnter = useCallback(() => setHovered(true), []); 7 | const onMouseLeave = useCallback(() => setHovered(false), []); 8 | 9 | useEffect(() => { 10 | if (ref.current) { 11 | ref.current.addEventListener('mouseenter', onMouseEnter); 12 | ref.current.addEventListener('mouseleave', onMouseLeave); 13 | 14 | return () => { 15 | ref.current?.removeEventListener('mouseenter', onMouseEnter); 16 | ref.current?.removeEventListener('mouseleave', onMouseLeave); 17 | }; 18 | } 19 | 20 | return undefined; 21 | }, []); 22 | 23 | return { ref, hovered }; 24 | } 25 | -------------------------------------------------------------------------------- /docs/docs/utilities/for.mdx: -------------------------------------------------------------------------------- 1 | # For 2 | 3 | The `For` component can iterate over arrays and render JSX for each available item. Keys are automatically assigned. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { For } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { ForDemo } from '../../demo/ForDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { For } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const data = [{name: 'React'}, {name: 'Haiku'}]; 22 | 23 | return( 24 | 25 | {`${index}: ${item.name}`} 26 | }/> 27 | ); 28 | } 29 | ``` 30 | 31 | ### API 32 | 33 | The component accepts the following props: 34 | 35 | - `each` - the array to iterate over when rendering 36 | - `render` - the render method that can return JSX -------------------------------------------------------------------------------- /lib/hooks/useOrientation.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | type Orientation = "portrait" | "landscape"; 4 | 5 | export const useOrientation = (): Orientation => { 6 | const [orientation, setOrientation] = useState(window.matchMedia("(orientation: portrait)").matches 7 | ? "portrait" 8 | : "landscape"); 9 | 10 | useEffect(() => { 11 | const handleOrientationChange = (e: MediaQueryListEvent) => { 12 | setOrientation(e.matches ? "portrait" : "landscape"); 13 | }; 14 | 15 | const portraitMediaQuery = window.matchMedia("(orientation: portrait)"); 16 | portraitMediaQuery.addEventListener("change", handleOrientationChange); 17 | 18 | return () => 19 | portraitMediaQuery.removeEventListener("change", handleOrientationChange); 20 | }, []); 21 | 22 | return orientation; 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "jsx": "react-jsx", 7 | "rootDir": "./lib", 8 | "baseUrl": "./", 9 | "allowJs": true, 10 | "checkJs": true, 11 | "declaration": true, 12 | "outDir": "./dist", 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitAny": true, 17 | "strictNullChecks": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noUncheckedIndexedAccess": true, 21 | "allowUnusedLabels": false, 22 | "allowUnreachableCode": false, 23 | "skipLibCheck": true, 24 | "types": ["@testing-library/jest-dom"] 25 | }, 26 | "include": ["lib/**/*"], 27 | "exclude": ["node_modules", "dist", "docs"] 28 | } 29 | -------------------------------------------------------------------------------- /docs/docs/hooks/useFirstRender.mdx: -------------------------------------------------------------------------------- 1 | # useFirstRender() 2 | 3 | The `useFirstRender()` hook lets you detect whether or not the component you use it on is on its initial render, it returns a boolean value with the result. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useFirstRender } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseFirstRenderDemo } from '../../demo/UseFirstRenderDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useFirstRender, useUrgentUpdate } from "react-haiku" 19 | 20 | export const Component = () => { 21 | const isFirst = useFirstRender() 22 | const update = useUrgentUpdate(); 23 | 24 | return ( 25 | <> 26 | First Render? - {isFirst ? 'Yes' : 'No'} 27 | 28 | 29 | ); 30 | } 31 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/useInputValue.mdx: -------------------------------------------------------------------------------- 1 | # useInputValue() 2 | 3 | The useInputValue() hook lets you easily manage states for inputs in your components 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useInputValue } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseInputValueDemo } from '../../demo/UseInputValueDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useInputValue } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const [nameValue, setNameValue] = useInputValue(''); 22 | 23 | return ( 24 | <> 25 | 30 |

{`Value - ${nameValue ? nameValue : 'None'}`}

31 | 32 | ); 33 | } 34 | ``` -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /docs/docs/hooks/useDeviceOS.mdx: -------------------------------------------------------------------------------- 1 | # useDeviceOS() 2 | 3 | The `useDeviceOS()` hook detects the user's operating system, including mobile emulators, and uses string manipulation for identifying unique or new OS versions. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useDeviceOS } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseDeviceOSDemo } from '../../demo/UseDeviceOSDemo.jsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | 22 | import { useDeviceOS } from 'react-haiku'; 23 | 24 | export const Component = () => { 25 | const deviceOS = useDeviceOS(); 26 | 27 | return ( 28 |
29 | Check Your Device OS! 30 |

OS: {deviceOS}

31 |
32 | ); 33 | } 34 | 35 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/useWindowSize.mdx: -------------------------------------------------------------------------------- 1 | # useWindowSize() 2 | 3 | The `useWindowSize()` hook provides the current window `width` and `height` dimensions. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useWindowSize } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseWindowSizeDemo } from '../../demo/UseWindowSizeDemo.jsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useWindowSize } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const { height, width } = useWindowSize(); 25 | 26 | return ( 27 |
28 | Resize Your Window! 29 | 30 |

Window Height: {height}

31 |

Window Width: {width}

32 |
33 | ); 34 | }; 35 | ``` 36 | -------------------------------------------------------------------------------- /lib/utils/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Children } from 'react'; 2 | import { If } from './If'; 3 | 4 | import { type ReactNode } from 'react'; 5 | 6 | type Props = { 7 | children: ReactNode & { 8 | props: { 9 | isTrue?: boolean; 10 | }; 11 | }; 12 | }; 13 | 14 | type ElseProps = { 15 | render?: () => ReactNode; 16 | children?: ReactNode; 17 | }; 18 | 19 | export const Show = ({ children }: Props) => { 20 | let when: ReactNode | null = null; 21 | let otherwise: ReactNode | null = null; 22 | 23 | Children.forEach(children, (child) => { 24 | if (child.props.isTrue === undefined) { 25 | otherwise = child; 26 | } else if (!when && child.props.isTrue === true) { 27 | when = child; 28 | } 29 | }); 30 | 31 | return (when || otherwise) as ReactNode; 32 | }; 33 | 34 | Show.When = If; 35 | Show.Else = ({ render, children }: ElseProps) => (render ? render() : children); 36 | -------------------------------------------------------------------------------- /docs/demo/UseNetworkDemo.tsx: -------------------------------------------------------------------------------- 1 | import { useNetwork } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseNetworkDemo = () => { 5 | const isOnline = useNetwork(); 6 | 7 | return ( 8 |
9 |

Network Status:

10 |
15 | {isOnline ? 'ONLINE' : 'OFFLINE'} 16 |
17 |

26 | Toggle network status in Chrome DevTools (Application → Service Workers 27 | → Offline) 28 |

29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /docs/docs/hooks/useSingleEffect.mdx: -------------------------------------------------------------------------------- 1 | # useSingleEffect() 2 | 3 | The `useSingleEffect()` hook works exactly like useEffect, except it is called only a single time when the component mounts. This helps with React's recent update to the useEffect hook which is being called twice on mount. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useSingleEffect } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseSingleEffectDemo } from '../../demo/UseSingleEffectDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { useSingleEffect } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [renderCount, setRenderCount] = useState(0); 23 | 24 | useSingleEffect(() => { 25 | setRenderCount(renderCount + 1); 26 | }) // no dependency array needed 27 | 28 | return Effect executed only {renderCount} time! 29 | } 30 | ``` -------------------------------------------------------------------------------- /docs/docs/utilities/image.mdx: -------------------------------------------------------------------------------- 1 | # Image 2 | 3 | The Image component provides automatic fallback handling for broken images. Use this for robust media display with graceful degradation. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { Image } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseImageDemo } from '../../demo/ImageDemo.tsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { Image } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | return ( 22 | Descriptive text 28 | ); 29 | }; 30 | ``` 31 | 32 | ### API 33 | 34 | This component extends img HTML attributes and adds: 35 | 36 | - `fallback` - Required string for fallback image URL 37 | - All standard img props (src, alt, loading, etc.) 38 | -------------------------------------------------------------------------------- /docs/docs/hooks/useScript.mdx: -------------------------------------------------------------------------------- 1 | # useScript() 2 | 3 | The `useScript()` hook allows appending script tags to your document from inside React components. The state variable returns a status that can have one of the following values: `idle`, `loading`, `ready`, `error`. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useScript } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseScriptDemo } from '../../demo/UseScriptDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useScript } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | // GSAP 22 | const script = useScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.10.4/gsap.min.js'); 23 | 24 | return ( 25 | <> 26 | Check the bottom of the body tag! 27 | 30 | 31 | ); 32 | } 33 | ``` -------------------------------------------------------------------------------- /lib/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 3 | 4 | export function useEventListener( 5 | eventName: string, 6 | handler: (e: Event) => any, 7 | element?: React.RefObject, 8 | ) { 9 | const savedHandler = useRef(handler); 10 | 11 | useIsomorphicLayoutEffect(() => { 12 | savedHandler.current = handler; 13 | }, [handler]); 14 | 15 | useEffect(() => { 16 | const targetElement = element?.current || window; 17 | 18 | if (!(targetElement && targetElement.addEventListener)) { 19 | return; 20 | } 21 | 22 | const eventListener = (event: Event) => savedHandler.current(event); 23 | targetElement.addEventListener(eventName, eventListener); 24 | 25 | return () => { 26 | targetElement.removeEventListener(eventName, eventListener); 27 | }; 28 | }, [eventName, element]); 29 | } 30 | -------------------------------------------------------------------------------- /lib/hooks/useScrollPosition.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useEventListener } from './useEventListener'; 3 | 4 | const getPosition = () => 5 | typeof window !== 'undefined' 6 | ? { x: window.scrollX, y: window.scrollY } 7 | : { x: 0, y: 0 }; 8 | 9 | const setPosition = ({ x, y }: { x?: number; y?: number }) => { 10 | if (typeof window !== 'undefined') { 11 | const scrollOptions: ScrollToOptions = { behavior: 'smooth' }; 12 | 13 | if (typeof x === 'number') scrollOptions.left = x; 14 | if (typeof y === 'number') scrollOptions.top = y; 15 | 16 | window.scrollTo(scrollOptions); 17 | } 18 | }; 19 | 20 | export function useScrollPosition() { 21 | const [currentPosition, setCurrentPosition] = useState(getPosition()); 22 | 23 | ['scroll', 'resize'].forEach((item) => 24 | useEventListener(item, () => setCurrentPosition(getPosition())), 25 | ); 26 | 27 | return [currentPosition, setPosition]; 28 | } 29 | -------------------------------------------------------------------------------- /docs/docs/hooks/useBatteryStatus.mdx: -------------------------------------------------------------------------------- 1 | # useBatteryStatus() 2 | 3 | The `useBatteryStatus()` hook provides real-time information about the device's battery level and charging status, automatically updating as these values change. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useBatteryStatus } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseBatteryStatusDemo } from '../../demo/UseBatteryStatusDemo.jsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | 22 | import { useBatteryStatus } from 'react-haiku'; 23 | 24 | export const Component = () => { 25 | const {level, isCharging} = useBatteryStatus(); 26 | 27 | return ( 28 |
29 |

Battery Level: {level}

30 |

Is Battery Charging: {isCharging ? "True" : "False"}

31 |
32 | ); 33 | } 34 | 35 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/usePreventBodyScroll.mdx: -------------------------------------------------------------------------------- 1 | # usePreventBodyScroll() 2 | 3 | The `usePreventBodyScroll()` hook disables body scrolling when active and restores it upon deactivation or component unmounting. It provides a boolean state, a setter, and a toggle function for dynamic scroll control. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { usePreventBodyScroll } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UsePreventBodyScrollDemo } from '../../demo/UsePreventBodyScrollDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { usePreventBodyScroll } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const { isScrollLocked, toggleScrollLock } = usePreventBodyScroll(); 22 | 23 | return ( 24 | <> 25 | Scroll: {isScrollLocked ? 'Disabled' : 'Enabled'} 26 | 27 | 28 | ); 29 | } 30 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/useIsomorphicLayoutEffect.mdx: -------------------------------------------------------------------------------- 1 | # useIsomorphicLayoutEffect() 2 | 3 | The `useIsomorphicLayoutEffect()` hook lets switch between using `useEffect` and `useLayoutEffect` depending on the execution environment. If your app uses server side rendering, the hook will run `useEffect`, otherwise it will run `useLayoutEffect`. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useIsomorphicLayoutEffect } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseIsomorphicLayoutEffectDemo } from '../../demo/UseIsomorphicLayoutEffectDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useIsomorphicLayoutEffect } from "react-haiku" 19 | 20 | export const Component = () => { 21 | useIsomorphicLayoutEffect(() => { 22 | // do whatever 23 | }, []) 24 | 25 | return ( 26 | <> 27 | SSR will run useEffect 28 | Browser will run useLayoutEffect 29 | 30 | ); 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /lib/utils/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentType, FC, ReactNode } from "react"; 2 | 3 | type CaseComponent = ReactNode | ComponentType; 4 | 5 | export interface SwitchProps { 6 | /** List of case components to be rendered when case matches */ 7 | components: Record; 8 | /** Default component to be rendered if the value does not match any of the given cases */ 9 | defaultComponent: CaseComponent; 10 | /** Value to check with the cases to render the corresponding component */ 11 | value: string | number; 12 | } 13 | 14 | /** 15 | * A component that renders one of the provided case components based on the value. 16 | */ 17 | export const Switch: FC = ({ components, defaultComponent, value }) => { 18 | let RenderedComponent = (components[value] || defaultComponent) as CaseComponent; 19 | 20 | if (typeof RenderedComponent === "function") { 21 | return ; 22 | } 23 | 24 | return <>{RenderedComponent}; 25 | }; -------------------------------------------------------------------------------- /lib/hooks/useConfirmExit.ts: -------------------------------------------------------------------------------- 1 | import { off, on } from '../helpers/event'; 2 | import { useCallback, useEffect } from 'react'; 3 | 4 | export function useConfirmExit( 5 | enabled: boolean | (() => boolean), 6 | message = 'Are you sure you want to exit?', 7 | ) { 8 | const handler = useCallback( 9 | (e: Event) => { 10 | const finalEnabled = typeof enabled === 'function' ? enabled() : true; 11 | 12 | if (!finalEnabled) { 13 | return; 14 | } 15 | 16 | e.preventDefault(); 17 | 18 | // NOTE: modern browsers no longer support custom messages with .returnValue 19 | if (message) { 20 | // @ts-ignore 21 | e.returnValue = message; 22 | } 23 | 24 | return message; 25 | }, 26 | [enabled, message], 27 | ); 28 | 29 | useEffect(() => { 30 | if (!enabled) { 31 | return; 32 | } 33 | 34 | on(window, 'beforeunload', handler); 35 | 36 | return () => off(window, 'beforeunload', handler); 37 | }, [enabled, handler]); 38 | } 39 | -------------------------------------------------------------------------------- /docs/demo/UseTimerDemo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTimer } from 'react-haiku'; 3 | 4 | export const UseTimerDemo = () => { 5 | const { time, isRunning, start, pause, reset } = useTimer({ 6 | startTime: 0, 7 | endTime: 10000, 8 | interval: 1000, 9 | }); 10 | 11 | return ( 12 |
13 |

Time: {time}

14 |
15 | 22 | 23 | 30 | 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /lib/hooks/useCookie.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import { useCookieListener } from './useCookieListener'; 4 | import { useSingleEffect } from './useSingleEffect'; 5 | 6 | import { 7 | getCookie, 8 | setCookie, 9 | deleteCookie, 10 | } from '../helpers/cookie'; 11 | 12 | export const useCookie = ( 13 | key: string, 14 | initialValue: T, 15 | expireDays = 365, 16 | ) => { 17 | const [cookieValue, setCookieValue] = useState( 18 | getCookie(key) ?? initialValue, 19 | ); 20 | 21 | useSingleEffect(() => { 22 | if (typeof getCookie(key) === "undefined") { 23 | setCookie(key, initialValue, expireDays); 24 | } 25 | }); 26 | 27 | useCookieListener( 28 | (value: T) => { 29 | setCookieValue(value); 30 | }, 31 | [key], 32 | ); 33 | 34 | const setValue = (value: T) => { 35 | setCookieValue(value); 36 | setCookie(key, value, expireDays); 37 | }; 38 | 39 | const deleteValue = () => { 40 | deleteCookie(key); 41 | }; 42 | 43 | return [cookieValue, setValue, deleteValue]; 44 | }; 45 | -------------------------------------------------------------------------------- /lib/hooks/usePreventBodyScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | enum ScrollState { 4 | SCROLL = '', 5 | LOCK = 'hidden', 6 | } 7 | 8 | type PreventBodyScrollResult = { 9 | isScrollLocked: boolean; 10 | setIsScrollLocked: React.Dispatch>; 11 | toggleScrollLock: () => void; 12 | }; 13 | 14 | export function usePreventBodyScroll(): PreventBodyScrollResult { 15 | const [isScrollLocked, setIsScrollLocked] = useState(false); 16 | 17 | useEffect(() => { 18 | if (isScrollLocked) { 19 | document.body.style.overflow = ScrollState.LOCK; 20 | } else { 21 | document.body.style.overflow = ScrollState.SCROLL; 22 | } 23 | 24 | return () => { 25 | document.body.style.overflow = ScrollState.SCROLL; 26 | }; 27 | }, [isScrollLocked]); 28 | 29 | const toggleScrollLock = () => setIsScrollLocked((prevState) => !prevState); 30 | 31 | return { 32 | isScrollLocked, 33 | setIsScrollLocked, 34 | toggleScrollLock, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /docs/demo/UseEventListenerDemo.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useEventListener } from "react-haiku"; 3 | import React from 'react'; 4 | 5 | export const UseEventListenerDemo = () => { 6 | const [countWindow, setCountWindow] = React.useState(0); 7 | const [countRef, setCountRef] = React.useState(0); 8 | 9 | // Button Ref 10 | const buttonRef = React.useRef(null) 11 | 12 | // Event Handlers 13 | const countW = () => setCountWindow(countWindow + 1); 14 | const countR = () => setCountRef(countRef + 1); 15 | 16 | // Example 1: Window Event 17 | useEventListener('scroll', countW); 18 | 19 | // Example 2: Element Event 20 | useEventListener('click', countR, buttonRef); 21 | 22 | return ( 23 |
24 | Window Event Triggered {countWindow} Times! 25 | Ref Event Triggered {countRef} Times! 26 | 27 |
28 | ); 29 | } -------------------------------------------------------------------------------- /lib/tests/hooks/usePrevious.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { usePrevious } from "../../hooks/usePrevious"; 3 | 4 | describe('usePrevious Hook', () => { 5 | test('returns the initial value initially', () => { 6 | const initialValue = "initial" 7 | 8 | const { result } = renderHook(() => usePrevious(initialValue)); 9 | expect(result.current).toBe(initialValue); 10 | }); 11 | 12 | test('returns the previous value', () => { 13 | const initialValue = "initial" 14 | 15 | const { result, rerender } = renderHook(({ value }) => usePrevious(value), { 16 | initialProps: { 17 | value: initialValue 18 | } 19 | }); 20 | 21 | expect(result.current).toBe(initialValue) 22 | 23 | act(() => { 24 | rerender({ value: "previous" }); 25 | }); 26 | 27 | expect(result.current).toBe(initialValue) 28 | 29 | act(() => { 30 | rerender({ value: "next" }); 31 | }); 32 | 33 | expect(result.current).toBe("previous") 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /docs/docs/hooks/useSize.mdx: -------------------------------------------------------------------------------- 1 | # useSize() 2 | 3 | The `useSize` hook observes a referenced DOM element and returns its current width and height, updating the values whenever the element is resized. This is useful for dynamically tracking size changes of any resizable component. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useSize } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseSizeDemo } from '../../demo/UseSizeDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | 19 | import { useRef } from "react" 20 | import { useSize } from "react-haiku" 21 | 22 | export const Component = () => { 23 | const elementRef = useRef(null); 24 | const { width, height } = useSize(elementRef); 25 | 26 | return ( 27 |
28 |

Window Height: {height}

29 |

Window Width: {width}

30 |
31 | ); 32 | } 33 | 34 | ``` 35 | 36 | ### API 37 | This hook accepts the following arguments: 38 | 39 | - `ref` - a reference to the DOM element you want to observe for size changes. -------------------------------------------------------------------------------- /lib/hooks/useDeviceOS.ts: -------------------------------------------------------------------------------- 1 | export const useDeviceOS = () => { 2 | // @ts-ignore 3 | const { platform } = navigator?.userAgentData || {}; 4 | 5 | if (!!platform) { 6 | return platform; 7 | } 8 | // For mobile emulators on browsers 9 | return checkOSBasedOnAgentInfo(navigator.userAgent); 10 | }; 11 | 12 | const checkOSBasedOnAgentInfo = (info: string) => { 13 | switch (true) { 14 | case info.includes('iPhone') || info.includes('iPad'): 15 | return 'iOS'; 16 | case info.includes('Linux'): 17 | return 'Linux'; 18 | case info.includes('Windows'): 19 | return 'Windows'; 20 | default: 21 | return extractUniqueOS(info); 22 | } 23 | }; 24 | 25 | const extractUniqueOS = (info: string) => { 26 | const regex = /\(([^)]+)\)/; 27 | const matches = info.match(regex); 28 | 29 | if (matches && matches.length > 1) { 30 | const deviceText = matches[1]; 31 | const firstWord = deviceText?.trim().split(' ')[0]; 32 | return firstWord?.slice(0, -1); 33 | } 34 | 35 | return 'Unknown'; 36 | }; 37 | -------------------------------------------------------------------------------- /docs/demo/UsePreviousDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState } from "react"; 3 | import { usePrevious } from "react-haiku" 4 | 5 | export const UsePreviousDemo = () => { 6 | const [count, setCount] = useState(0); 7 | const prevCount = usePrevious(count); 8 | 9 | function handleIncrement() { 10 | setCount(prev => prev + 1) 11 | } 12 | 13 | function handleDecrement() { 14 | setCount(prev => prev - 1) 15 | } 16 | 17 | return ( 18 |
19 |

Current Value: {count}

20 |

Previous Value: {prevCount}

21 | 22 |
23 | 26 | 29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /docs/docs/hooks/useUpdateEffect.mdx: -------------------------------------------------------------------------------- 1 | # useUpdateEffect() 2 | 3 | The `useUpdateEffect()` hook will work exactly like a `useEffect()` hook, except it will skip the first render and only react to changes for values passed inside its dependency array after the initial render. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useUpdateEffect } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseUpdateEffectDemo } from '../../demo/UseUpdateEffectDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { useUpdateEffect } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [count, setCount] = useState(0); 23 | const [triggerCount, setTriggerCount] = useState(0); 24 | 25 | useUpdateEffect(() => { 26 | setTriggerCount(triggerCount + 1); 27 | }, [count]) 28 | 29 | return ( 30 | <> 31 | Updates Detected: {triggerCount} 32 | 33 | 34 | ); 35 | } 36 | ``` -------------------------------------------------------------------------------- /docs/LICENSE.md: -------------------------------------------------------------------------------- 1 | React Haiku by David Haz 2 | 3 | Copyright 2025 David Haz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | React Haiku by David Haz 2 | 3 | Copyright 2025 David Haz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /docs/demo/UseSizeDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useSize} from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const UseSizeDemo = () => { 5 | const elementRef = React.useRef(null); 6 | const { width, height } = useSize(elementRef); 7 | return ( 8 |
9 | Resize Me! 10 |
23 |

Width: {width}px

24 |

Height: {height}px

25 |
26 |
27 | ); 28 | } -------------------------------------------------------------------------------- /docs/demo/UsePermissionDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState } from "react"; 3 | import { usePermission } from "react-haiku" 4 | 5 | export const UsePermissionDemo = () => { 6 | const state = usePermission("geolocation") 7 | const [location, setLocation] = useState(null) 8 | 9 | function handleGetCurrentPosition() { 10 | if (state !== "prompt" && state !== "granted") return 11 | 12 | navigator.geolocation.getCurrentPosition((location) => { 13 | setLocation(location) 14 | }) 15 | } 16 | 17 | return ( 18 |
19 |

Permission state: {state}

20 | 21 |
22 |         {JSON.stringify(location ?? {}, null, 2)}
23 |       
24 | 25 |
26 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /docs/docs/hooks/useScrollPosition.mdx: -------------------------------------------------------------------------------- 1 | # useScrollPosition() 2 | 3 | The `useScrollPosition()` hook allows you to fetch the window's scroll height/width in real time and to programatically set them by using the provided method. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useScrollPosition } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseScrollPositionDemo } from '../../demo/UseScrollPositionDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useScrollPosition } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const [scroll, setScroll] = useScrollPosition(); 22 | 23 | return ( 24 | <> 25 | Current Position: {`X: ${scroll.x}, Y: ${scroll.y}`}! 26 | 29 | 30 | ); 31 | } 32 | ``` 33 | 34 | ### API 35 | 36 | - `scroll` – contains the x and y values of the window's scroll position 37 | - `setScroll` - used for programatically scrolling inside the active viewport -------------------------------------------------------------------------------- /docs/demo/ShowDemo.jsx: -------------------------------------------------------------------------------- 1 | import { Show } from "react-haiku" 2 | import React from 'react'; 3 | 4 | export const ShowDemo = () => { 5 | const [number, setNumber] = React.useState(6); 6 | 7 | return ( 8 |
9 | 10 | 11 | Number is 6! 12 | 13 | 14 | 15 | Number is 7! 16 | 17 | 18 | 19 | No valid number found! 20 | 21 | 22 | 23 |
24 | ); 25 | } -------------------------------------------------------------------------------- /lib/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useClipboard({ timeout = 500 } = {}) { 4 | const [error, setError] = useState(null); 5 | const [copied, setCopied] = useState(false); 6 | const [copyTimeout, setCopyTimeout] = useState(undefined); 7 | 8 | const handleCopyResult = (hasError: boolean) => { 9 | clearTimeout(copyTimeout); 10 | 11 | setCopyTimeout( 12 | setTimeout(() => setCopied(false), timeout) as unknown as number, 13 | ); 14 | 15 | setCopied(hasError); 16 | }; 17 | 18 | const copy = (value: string) => { 19 | if ('clipboard' in navigator) { 20 | navigator.clipboard 21 | .writeText(value) 22 | .then(() => handleCopyResult(true)) 23 | .catch((err) => setError(err)); 24 | } else { 25 | setError(new Error('Error: navigator.clipboard is not supported')); 26 | } 27 | }; 28 | 29 | const reset = () => { 30 | setError(null); 31 | setCopied(false); 32 | clearTimeout(copyTimeout); 33 | }; 34 | 35 | return { copy, reset, error, copied }; 36 | } 37 | -------------------------------------------------------------------------------- /docs/docs/hooks/useClipboard.mdx: -------------------------------------------------------------------------------- 1 | # useClipboard() 2 | 3 | The useClipboard() hook will help you interact with the browser's navigator.clipboard property in order to copy text to the user's clipboard. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useClipboard } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseClipboardDemo } from '../../demo/UseClipboardDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useClipboard } from 'react-haiku'; 19 | 20 | const Component = () => { 21 | const clipboard = useClipboard({ timeout: 2000 }); 22 | 23 | return ( 24 | 27 | ); 28 | } 29 | ``` 30 | 31 | ### API 32 | 33 | The useClipboard hook takes an argument `options` where you can set the timeout duration for a successful copy operation 34 | 35 | - `copy` – method use to copy a string to the clipboard 36 | - `copied` – boolean value that indicates a successful copy 37 | - `reset` – method that can clear any timeout and reset the `copied` value 38 | - `error` – error handling object returned if any potential errors occur -------------------------------------------------------------------------------- /docs/static/img/sponsor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/docs/hooks/useClickOutside.mdx: -------------------------------------------------------------------------------- 1 | # useClickOutside() 2 | 3 | The useClickOutside() hook lets you trigger a callback whenever the user clicks outside of a target element 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useClickOutside } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseClickOutsideDemo } from '../../demo/UseClickOutsideDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState, useRef } from 'react' 19 | import { useClickOutside } from "react-haiku" 20 | 21 | export const Component = () => { 22 | const [count, setCount] = useState(0); 23 | const ref = useRef(null) 24 | 25 | const handleClickOutside = () => setCount(count + 1); 26 | 27 | useClickOutside(ref, handleClickOutside); 28 | 29 | return ( 30 | <> 31 | Clicked Outside {count} Times! 32 | 33 | 34 | ); 35 | } 36 | ``` 37 | 38 | ### API 39 | 40 | This hook accepts the following arguments: 41 | - `ref` - the target element, clicking outside of this element will trigger the handler 42 | - `handler` - the handler for what happens when you click outside of the target -------------------------------------------------------------------------------- /docs/docs/hooks/useLocalStorage.mdx: -------------------------------------------------------------------------------- 1 | # useLocalStorage() 2 | 3 | The `useLocalStorage()` hook is a quick way to set, read, and manage `localStorage` values. It comes with automatic JSON serialization/deserialization. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useLocalStorage } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseLocalStorageDemo } from '../../demo/UseLocalStorageDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useEffect } from 'react' 19 | import { useLocalStorage } from "react-haiku" 20 | 21 | export const Component = () => { 22 | const [value, setValue] = useLocalStorage('message'); 23 | 24 | useEffect(() => { 25 | setValue({ message: 'Hello!' }) 26 | }, []) 27 | 28 | return ( 29 | <> 30 | Storage Value: {value.message} 31 | 32 | 33 | ); 34 | } 35 | ``` 36 | 37 | ### API 38 | 39 | This hook accetps the following arguments: 40 | 41 | - `key` - the key to use for a localStorage entry 42 | - `value` - the value corresponding to a certain key, can be a a simple string value or an object -------------------------------------------------------------------------------- /lib/hooks/useMousePosition.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | export function useMousePosition() { 4 | const [position, setPosition] = useState({ x: 0, y: 0 }); 5 | const { max } = Math; 6 | const target = useRef(); 7 | 8 | const setMousePosition = (event: MouseEvent) => { 9 | if (target.current) { 10 | // @ts-ignore 11 | const r = event?.currentTarget?.getBoundingClientRect(); 12 | const x = max( 13 | 0, 14 | Math.round( 15 | event.pageX - r.left - (window.pageXOffset || window.scrollX), 16 | ), 17 | ); 18 | const y = max( 19 | 0, 20 | Math.round( 21 | event.pageY - r.top - (window.pageYOffset || window.scrollY), 22 | ), 23 | ); 24 | setPosition({ x, y }); 25 | } else setPosition({ x: event.clientX, y: event.clientY }); 26 | }; 27 | 28 | useEffect(() => { 29 | const element = target?.current ? target.current : document; 30 | element.addEventListener('mousemove', setMousePosition); 31 | 32 | return () => element.removeEventListener('mousemove', setMousePosition); 33 | }, [target.current]); 34 | 35 | return { target, ...position }; 36 | } 37 | -------------------------------------------------------------------------------- /docs/docs/hooks/useOrientation.mdx: -------------------------------------------------------------------------------- 1 | # useOrientation() 2 | 3 | The useOrientation() hook detects and tracks device screen orientation changes. Use this when you need to adapt your UI layout based on portrait/landscape modes. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useOrientation } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseOrientationDemo } from '../../demo/UseOrientationDemo.tsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useOrientation } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const orientation = useOrientation(); 25 | 26 | return ( 27 | <> 28 |

Current Orientation:

29 |
32 | {orientation.toUpperCase()} 33 |
34 | 35 | ); 36 | }; 37 | ``` 38 | 39 | ### API 40 | 41 | #### Arguments 42 | 43 | - This hook requires no arguments 44 | 45 | #### Returns 46 | 47 | - orientation - A string type union of either "portrait" or "landscape" 48 | -------------------------------------------------------------------------------- /docs/docs/utilities/if.mdx: -------------------------------------------------------------------------------- 1 | # If 2 | 3 | The `If` component can be used for simple conditional rendering. It will render its children whenever the condition passed to the `isTrue` prop is truthy. For more complex rendering logic, you can use the `Show` component 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { If } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { IfDemo } from '../../demo/IfDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { If } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [number, setNumber] = useState(6); 23 | 24 | return( 25 | <> 26 | Click to update state! 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | ``` 37 | 38 | ### API 39 | 40 | The `If` component accepts the following props: 41 | 42 | - `isTrue` - the condition to evaluate when rendering the component's contents -------------------------------------------------------------------------------- /docs/docs/hooks/useMediaQuery.mdx: -------------------------------------------------------------------------------- 1 | # useMediaQuery() 2 | 3 | The `useMediaQuery()` hook allows you to react to media queries inside of your React components. It accepts a `media query` argument and returns `true` or `false` when the query is a match or not with your current browser's properties. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useMediaQuery } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseMediaQueryDemo } from '../../demo/UseMediaQueryDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useMediaQuery } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const breakpoint = useMediaQuery('(max-width: 1200px)'); 22 | 23 | return ( 24 | <> 25 |

Resize your window!

26 | 29 | 30 | ); 31 | } 32 | ``` 33 | 34 | ### API 35 | 36 | - `initialValue` – Boolean value to specify the initial value of the query matches result 37 | 38 | ```jsx 39 | const breakpoint = useMediaQuery('(max-width: 1000px)', false); 40 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/usePrevious.mdx: -------------------------------------------------------------------------------- 1 | # usePrevious() 2 | 3 | The `usePrevious()` tracks and returns the previous value of a given input 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { usePrevious } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UsePreviousDemo } from '../../demo/UsePreviousDemo.tsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { usePrevious } from 'react-haiku'; 19 | 20 | export const Component = () => { 21 | const [count, setCount] = useState(0); 22 | const prevCount = usePrevious(count); 23 | 24 | function handleIncrement() { 25 | setCount(prev => prev + 1) 26 | } 27 | 28 | function handleDecrement() { 29 | setCount(prev => prev - 1) 30 | } 31 | 32 | return ( 33 |
34 |

Current Value: {count}

35 |

Previous Value: {prevCount}

36 | 37 |
38 | 41 | 44 |
45 |
46 | ); 47 | } 48 | ``` 49 | 50 | ### API 51 | 52 | #### Arguments 53 | 54 | - `value` - The new value to track and return the previous of 55 | -------------------------------------------------------------------------------- /docs/docs/hooks/useConfirmExit.mdx: -------------------------------------------------------------------------------- 1 | # useConfirmExit() 2 | 3 | The useConfirmExit() hook lets you display a prompt to the user before he closes the current tab depending on whether the tab is declared to be dirty or not. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useConfirmExit } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseConfirmExitDemo } from '../../demo/UseConfirmExitDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useConfirmExit, useBoolToggle } from "react-haiku" 19 | 20 | export const Component = () => { 21 | const [dirty, toggleDirty] = useBoolToggle(); 22 | 23 | useConfirmExit(dirty); 24 | 25 | return ( 26 | <> 27 | Try to close this tab with the window dirty! 28 |

Dirty: {`${dirty}`}

29 | 30 | 31 | ); 32 | } 33 | ``` 34 | 35 | ### API 36 | 37 | This hook accepts the following arguments: 38 | - `dirty` - the value to check when activating/deactivating the exit confirmation message 39 | - `message` - while this is availabe, using it currently has no effect since most browsers have disabled the feature of adding custom confirmation messages on this type of event -------------------------------------------------------------------------------- /lib/hooks/useIdle.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | 3 | const defaultEvents = [ 4 | 'keypress', 5 | 'mousemove', 6 | 'touchmove', 7 | 'click', 8 | 'scroll', 9 | ] as const; 10 | type DefaultEvent = (typeof defaultEvents)[number]; 11 | type Options = { 12 | events?: DefaultEvent[]; 13 | initialState?: boolean; 14 | }; 15 | const defaultOptions: Options = { 16 | events: [...defaultEvents], 17 | initialState: true, 18 | }; 19 | 20 | export function useIdle(timeout: number, options: Options = {}) { 21 | const { events, initialState }: Options = { ...defaultOptions, ...options }; 22 | const [idle, setIdle] = useState(initialState); 23 | const timer = useRef(undefined); 24 | 25 | useEffect(() => { 26 | const handleEvents = () => { 27 | setIdle(false); 28 | 29 | if (timer.current) { 30 | window.clearTimeout(timer.current); 31 | } 32 | 33 | timer.current = window.setTimeout(() => setIdle(true), timeout); 34 | }; 35 | 36 | events?.forEach((event) => document.addEventListener(event, handleEvents)); 37 | 38 | return () => 39 | events?.forEach((event) => 40 | document.removeEventListener(event, handleEvents), 41 | ); 42 | }, [timeout]); 43 | 44 | return idle; 45 | } 46 | -------------------------------------------------------------------------------- /docs/docs/hooks/useKeyPress.mdx: -------------------------------------------------------------------------------- 1 | # useKeyPress() 2 | 3 | The `useKeyPress()` hook listens for a specific combination of keys and runs a callback when they are all pressed. It normalizes keys for case-insensitive matching and handles cases like key holding or focus loss to ensure smooth behavior. 4 | 5 | ### Import 6 | 7 | 8 | ```jsx 9 | import { useKeyPress } from 'react-haiku'; 10 | ``` 11 | 12 | ### Usage 13 | 14 | import { UseKeyPressDemo } from '../../demo/UseKeyPressDemo.jsx'; 15 | 16 | 17 | 18 | ```jsx 19 | import { useKeyPress } from 'react-haiku'; 20 | 21 | const Component = () => { 22 | const [didKeyPress, setDidKeyPress] = useState(false); 23 | useKeyPress(['Control', 'Shift', 'A'], (e) => { 24 | setDidKeyPress(true); 25 | }); 26 | 27 | return ( 28 |
29 |

Press Control + Shift + A

30 | {didKeyPress &&

{`You pressed : Control + Shift + A`}

} 31 |
32 | ); 33 | }; 34 | 35 | export default App; 36 | ``` 37 | 38 | ### API 39 | This hook accepts the following arguments: 40 | - `keys` - An array of key names (case-insensitive) that should be pressed to trigger the callback 41 | - `callback` - The function to be executed when specified keys are pressed. 42 | 43 | ### Returns 44 | - `void` - This hook does not return anything. -------------------------------------------------------------------------------- /docs/demo/ErrorBoundaryDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ErrorBoundary } from 'react-haiku'; 3 | 4 | interface FallbackProps { 5 | retry: () => void; 6 | error: Error | null; 7 | errorInfo: React.ErrorInfo | null; 8 | } 9 | 10 | const Fallback: React.FC = ({ retry }) => { 11 | return ( 12 |
13 | We faced an error 14 | 21 |
22 | ); 23 | }; 24 | 25 | // Component that will intentionally cause an error 26 | const CrashComponent: React.FC = () => { 27 | // Only throw an error if window is defined (i.e., in a browser environment) 28 | // This prevents the error during Docusaurus build (SSR) 29 | if (typeof window !== 'undefined') { 30 | throw new Error('This is a test error inside the ErrorBoundary!'); 31 | } 32 | return

This component would normally throw an error to demonstrate the ErrorBoundary.

; 33 | }; 34 | 35 | export const ErrorBoundaryDemo: React.FC = () => { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /lib/utils/Class.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, ElementType } from "react"; 2 | interface ClassProps { 3 | className?: string; 4 | condition?: boolean; 5 | toggleClass?: string; 6 | children: ReactNode; 7 | as?: ElementType; 8 | [key: string]: any; 9 | } 10 | 11 | /** 12 | * Class component that conditionally applies a class name to a specified HTML element. 13 | * 14 | * @param className - The initial class name for the element. 15 | * @param condition - The condition to determine whether to apply the toggle class or not. 16 | * @param toggleClass - The class name to be toggled based on the condition. 17 | * @param children - The content to be rendered inside the specified HTML element. 18 | * @param as - The type of HTML element to render. 19 | * @param props - Any additional props to be passed to the element. 20 | * @returns The rendered Class component. 21 | */ 22 | export const Class: React.FC = ({ 23 | className = "", 24 | condition = false, 25 | toggleClass = "", 26 | children, 27 | as: Component = "div", 28 | ...props 29 | }) => { 30 | const computedClassName = condition 31 | ? `${className} ${toggleClass}`.trim() 32 | : className; 33 | 34 | return ( 35 | 36 | {children} 37 | 38 | ); 39 | }; -------------------------------------------------------------------------------- /docs/docs/hooks/useNetwork.mdx: -------------------------------------------------------------------------------- 1 | # useNetwork() 2 | 3 | The useNetwork() hook tracks network connectivity status. Use this to show offline/online indicators or handle connection changes in your application. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useNetwork } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseNetworkDemo } from '../../demo/UseNetworkDemo.tsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useNetwork } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const isOnline = useNetwork(); 25 | 26 | return ( 27 |
28 |

Network Status:

29 |
30 | {isOnline ? 'ONLINE' : 'OFFLINE'} 31 |
32 |

33 | Toggle network status in Chrome DevTools (Application → Service Workers 34 | → Offline) 35 |

36 |
37 | ); 38 | }; 39 | ``` 40 | 41 | ### API 42 | 43 | #### Arguments 44 | 45 | - This hook requires no arguments 46 | 47 | #### Returns 48 | 49 | - `isOnline` - Boolean indicating network connectivity status (true = online, false = offline) 50 | -------------------------------------------------------------------------------- /docs/docs/hooks/useScreenSize.mdx: -------------------------------------------------------------------------------- 1 | # useScreenSize() 2 | 3 | The `useScreenSize()` hook allows responsive breakpoint detection in components. It returns helper methods (`equals`, `lessThan`, `greaterThan` , `greaterThanEqual` , `lessThanEqual` ) and a string method (`toString`) representing the current screen size: `xs`, `sm`, `md`, `lg`, `xl`, or `2xl for size bigger than 1535 pixel`. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useScreenSize } from 'react-haiku'; 9 | ``` 10 | 11 | 12 | ### Usage 13 | 14 | import { UseScreenSizeDemo } from '../../demo/UseScreenSizeDemo.jsx'; 15 | 16 | 17 | 18 | ```jsx 19 | import { useScreenSize } from 'react-haiku'; 20 | 21 | export const MyComponent = () => { 22 | const screenSize = useScreenSize(); 23 | 24 | return ( 25 |
26 |

Current Screen Size: {screenSize.toString()}

27 |

Is the screen size medium ? {screenSize.eq("md") ? "Yes" : "No"}

28 |

Is the screen size less than large ? {screenSize.lt("lg") ? "Yes" : "No"}

29 |

Is the screen size greater than small ? {screenSize.gt("sm") ? "Yes" : "No"}

30 |

Is the screen size greater than or equal to small ? {screenSize.gte("sm") ? "Yes" : "No"}

31 |

Is the screen size less than or equal to small ? {screenSize.lte("sm") ? "Yes" : "No"}

32 |
33 | ); 34 | }; 35 | ``` -------------------------------------------------------------------------------- /docs/docs/hooks/useHold.mdx: -------------------------------------------------------------------------------- 1 | # useHold() 2 | 3 | The `useHold()` hook lets you detect long presses (holds) on target elements and trigger a handler after a set timeout is elapsed while the user is still holding down (click/touch) the element. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useHold } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseHoldDemo } from '../../demo/UseHoldDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react' 19 | import { useHold } from "react-haiku" 20 | 21 | export const Component = () => { 22 | const [count, setCount] = useState(0) 23 | const handleHold = () => setCount(count + 1); 24 | 25 | const buttonHold = useHold(handleHold, { delay: 2000 }); 26 | 27 | return ( 28 | <> 29 | Successful Holds: {count} 30 | 33 | 34 | ); 35 | } 36 | ``` 37 | 38 | ### API 39 | 40 | This hook accepts the following arguments: 41 | 42 | - `callback` - the handler function to execute on successful holds 43 | - `options` - the options object you can pass in 44 | - - `doPreventDefault` - boolean, whether or not to prevent the default event 45 | - - `delay` - number, a value in milliseconds, the amount of time the user needs to hold an element to trigger an event -------------------------------------------------------------------------------- /docs/docs/hooks/useMousePosition.mdx: -------------------------------------------------------------------------------- 1 | # useMousePosition() 2 | 3 | The `useMousePosition()` hook lets you track the mouse position when hovering over a specific container or the entire page, so if a target container is not provided through the `ref`, it will track the mouse position relative to the entire document. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useMousePosition } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseMousePositionDemo } from '../../demo/UseMousePositionDemo.jsx'; 14 | 15 | #### Targeted Container 16 | 17 | 18 | 19 | ```jsx 20 | import { useMousePosition } from 'react-haiku'; 21 | 22 | export const Component = () => { 23 | const { target, x, y } = useMousePosition(); 24 | 25 | return ( 26 |
27 | Hover This Container 28 |

{`X: ${x} | Y: ${y}`}

29 |
30 | ); 31 | } 32 | ``` 33 | 34 | #### Entire Document 35 | 36 | import { UseMousePositionFullDemo } from '../../demo/UseMousePositionDemo.jsx'; 37 | 38 | 39 | 40 | ```jsx 41 | import { useMousePosition } from 'react-haiku'; 42 | 43 | export const Component = () => { 44 | const { x, y } = useMousePosition(); 45 | 46 | return ( 47 |
48 |

{`X: ${x} | Y: ${y}`}

49 |
50 | ); 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haiku-docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids" 15 | }, 16 | "dependencies": { 17 | "@docusaurus/core": "2.0.0-beta.20", 18 | "@docusaurus/plugin-google-analytics": "^2.0.0-beta.20", 19 | "@docusaurus/preset-classic": "2.0.0-beta.20", 20 | "@mdx-js/react": "^1.6.22", 21 | "@react-spring/web": "^9.7.4", 22 | "clsx": "^1.1.1", 23 | "prism-react-renderer": "^1.3.1", 24 | "react": "^17.0.2", 25 | "react-dom": "^17.0.2", 26 | "react-haiku": "2.4.1", 27 | "react-hot-toast": "^2.4.1", 28 | "swiper": "^11.1.9" 29 | }, 30 | "devDependencies": { 31 | "@docusaurus/module-type-aliases": "2.0.0-beta.20" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /docs/src/components/Animations/AnimatedContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSpring, animated } from '@react-spring/web'; 3 | 4 | export const AnimatedContainer = ({ children, direction, reverse = false, endPosition = '0', skipObserver = false }) => { 5 | const [inView, setInView] = React.useState(false); 6 | const ref = React.useRef(); 7 | 8 | React.useEffect(() => { 9 | const observer = new IntersectionObserver( 10 | ([entry]) => { 11 | if (entry.isIntersecting) { 12 | setInView(true); 13 | observer.unobserve(ref.current); // Unobserve after triggering the animation 14 | } 15 | }, 16 | { threshold: 0.1 } 17 | ); 18 | 19 | observer.observe(ref.current); 20 | 21 | return () => observer.disconnect(); 22 | }, []); 23 | 24 | const directions = { 25 | vertical: "Y", 26 | horizontal: "X" 27 | } 28 | 29 | const springProps = useSpring({ 30 | from: { transform: `translate${directions[direction]}(${reverse ? '-100px' : '100px'})` }, 31 | to: inView || skipObserver ? { transform: `translate${directions[direction]}(${endPosition}px)` } : `translate${directions[direction]}(${reverse && '-'}100px)`, 32 | config: { tension: 50, friction: 25 }, 33 | }); 34 | 35 | return ( 36 | 37 | {children} 38 | 39 | ); 40 | }; -------------------------------------------------------------------------------- /docs/docs/hooks/useDebounce.mdx: -------------------------------------------------------------------------------- 1 | # useDebounce() 2 | 3 | The useDebounce() hook lets you debounce value changes inside your components. Use this when you want to perform a heavy operation based on state 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useDebounce } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseDebounceDemo } from '../../demo/UseDebounceDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState, useEffect } from 'react' 19 | import { useDebounce } from "react-haiku" 20 | 21 | export const Component = () => { 22 | const [value, setValue] = useState('') 23 | const debouncedValue = useDebounce(value, 1000) 24 | 25 | const handleChange = (event) => setValue(event.target.value) 26 | 27 | // Handle Change After Debounce 28 | useEffect(() => { 29 | console.log(debouncedValue); 30 | }, [debouncedValue]) 31 | 32 | return ( 33 | <> 34 | Real-Time Value: {value} 35 | Debounced value: {debouncedValue} 36 | 37 | 38 | 39 | ); 40 | } 41 | ``` 42 | 43 | ### API 44 | 45 | This hook accepts the following arguments: 46 | - `value` - the state value that you want to debounce on changes 47 | - `timeout` - the amount of time in milliseconds to wait until the debounce is triggered, `500ms` by default -------------------------------------------------------------------------------- /lib/tests/utils/Switch.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import { Switch } from '../../utils/Switch'; 3 | 4 | const ComponentA = () =>
Component A
; 5 | const ComponentB = () =>
Component B
; 6 | const DefaultComponent = () =>
Default Component
; 7 | 8 | enum TestCases { 9 | A = 'a', 10 | B = 'b', 11 | DEFAULT = 'default' 12 | } 13 | 14 | const renderSwitch = (value: TestCases) => { 15 | return render( 16 | 24 | ); 25 | }; 26 | 27 | describe('Switch Component', () => { 28 | it('should render the correct component based on value', () => { 29 | const { getByText, asFragment } = renderSwitch(TestCases.A); 30 | expect(getByText('Component A')).toBeInTheDocument(); 31 | expect(asFragment()).toMatchSnapshot(); 32 | }); 33 | 34 | it('should render the default component if value does not match any case', () => { 35 | const { getByText, asFragment } = renderSwitch(TestCases.DEFAULT); 36 | expect(getByText('Default Component')).toBeInTheDocument(); 37 | expect(asFragment()).toMatchSnapshot(); 38 | }); 39 | }); -------------------------------------------------------------------------------- /lib/hooks/useMediaQuery.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | 3 | function attachMediaListener( 4 | query: any, 5 | callback: (event: MediaQueryListEvent) => any, 6 | ) { 7 | try { 8 | query.addEventListener('change', callback); 9 | 10 | return () => query.removeEventListener('change', callback); 11 | } catch (e) { 12 | query.addListener(callback); 13 | 14 | return () => query.removeListener(callback); 15 | } 16 | } 17 | 18 | function computeInitialValue(query: string, initialValue: T) { 19 | if (initialValue !== undefined) { 20 | return initialValue; 21 | } 22 | 23 | if (typeof window !== 'undefined' && 'matchMedia' in window) { 24 | return window.matchMedia(query).matches; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | export function useMediaQuery(query: string, initialValue: T) { 31 | const [matches, setMatches] = useState( 32 | computeInitialValue(query, initialValue), 33 | ); 34 | const ref = useRef(); 35 | 36 | useEffect(() => { 37 | if ('matchMedia' in window) { 38 | ref.current = window.matchMedia(query); 39 | setMatches(ref.current.matches); 40 | return attachMediaListener(ref.current, (event: MediaQueryListEvent) => 41 | setMatches(event.matches), 42 | ); 43 | } 44 | 45 | return undefined; 46 | }, [query]); 47 | 48 | return matches; 49 | } 50 | -------------------------------------------------------------------------------- /docs/demo/UseIntervalDemo.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useInterval } from 'react-haiku'; 3 | import React from 'react'; 4 | 5 | export const UseIntervalDemo = () => { 6 | const [count, setCount] = useState(0); 7 | const { start, stop } = useInterval(() => { 8 | setCount((c) => c + 1); 9 | }, 1000); 10 | 11 | const handleRestart = () => { 12 | stop(); 13 | setTimeout(() => { 14 | setCount(0); 15 | start(1000); 16 | }, 50); 17 | }; 18 | 19 | return ( 20 |
21 |

Interval Counter:

22 |
26 | {count} 27 |
28 | 29 |
30 | 33 | 36 |
37 | 38 |

46 | Restart will reset counter and interval 47 |

48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /docs/docs/hooks/useCookieListener.mdx: -------------------------------------------------------------------------------- 1 | # useCookieListener() 2 | 3 | The useCookieListener() hook monitors cookie changes and executes callbacks when specified cookies update. Use this to synchronize state across tabs/windows or react to authentication changes. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useCookieListener } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseCookieListenerDemo } from '../../demo/UseCookieListenerDemo.tsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useState } from 'react'; 22 | import { useCookieListener } from 'react-haiku'; 23 | 24 | export const Component = () => { 25 | const [lastChange, setLastChange] = useState(''); 26 | 27 | useCookieListener( 28 | (value, key) => { 29 | setLastChange(`${key} changed to: ${value}`); 30 | }, 31 | ['important_cookie'], 32 | ); 33 | 34 | return ( 35 | <> 36 |

Last change: {lastChange || 'None'}

37 | 40 | 41 | ); 42 | }; 43 | ``` 44 | 45 | ### API 46 | 47 | #### Arguments 48 | 49 | - `effect` - Callback function receiving (newValue, cookieKey) 50 | - `cookies` - Array of cookie names to monitor 51 | -------------------------------------------------------------------------------- /docs/static/img/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/hooks/useCookieListener.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | import { parseToCookieType, getCookies, parseToDataType } from '../helpers/cookie'; 4 | 5 | export const useCookieListener = ( 6 | effect: (a: T, b: string) => void, 7 | cookies: string[], 8 | ) => { 9 | const cookieValues = useRef>(getCookies(cookies)); 10 | 11 | useEffect(() => { 12 | const cookieOnChange = () => { 13 | const currentCookiesValues = getCookies(cookies); 14 | 15 | Object.entries(cookieValues.current).forEach( 16 | ([cookieKey, cookieValue]) => { 17 | const currentCookie = currentCookiesValues[cookieKey]; 18 | 19 | if ( 20 | parseToCookieType(currentCookie) !== parseToCookieType(cookieValue) 21 | ) { 22 | cookieValues.current = { 23 | ...cookieValues.current, 24 | [cookieKey]: currentCookie, 25 | }; 26 | 27 | const parsedValue = parseToDataType( 28 | parseToCookieType(currentCookie), 29 | ); 30 | if (parsedValue !== undefined) { 31 | effect(parsedValue, cookieKey); 32 | } 33 | } 34 | }, 35 | ); 36 | }; 37 | 38 | const cookieInterval = setInterval(cookieOnChange, 1000); 39 | 40 | return () => { 41 | clearInterval(cookieInterval); 42 | }; 43 | }, [effect, cookies]); 44 | }; 45 | -------------------------------------------------------------------------------- /docs/docs/hooks/useInterval.mdx: -------------------------------------------------------------------------------- 1 | # useInterval() 2 | 3 | The useInterval() hook provides managed interval execution with start/stop controls. Use this for recurring tasks like polls, animations, or delayed state updates. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useInterval } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseIntervalDemo } from '../../demo/UseIntervalDemo.tsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { useInterval } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [count, setCount] = useState(0); 23 | const { start, stop } = useInterval(() => { 24 | setCount((c) => c + 1); 25 | }, 1000); 26 | 27 | const handleRestart = () => { 28 | stop(); 29 | setTimeout(() => { 30 | setCount(0); 31 | start(1000); 32 | }, 50); 33 | }; 34 | 35 | return ( 36 | <> 37 |

Count: {count}

38 | 39 | 40 | 41 | ); 42 | }; 43 | ``` 44 | 45 | ### API 46 | 47 | #### Arguments 48 | 49 | - `callback` - Function to execute at each interval (required) 50 | - `initialDelay` - Starting interval duration in milliseconds (required) 51 | 52 | #### Returns 53 | 54 | - `start` - Function that (re)starts the interval, accepts optional delay parameter 55 | - `stop` - Function that cancels the current interval 56 | -------------------------------------------------------------------------------- /lib/utils/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ErrorBoundaryProps { 4 | children: React.ReactNode; 5 | fallback: React.ComponentType<{ 6 | error: Error | null; 7 | errorInfo: React.ErrorInfo | null; 8 | retry: () => void; 9 | }>; 10 | } 11 | 12 | interface ErrorBoundaryState { 13 | hasError: boolean; 14 | error: Error | null; 15 | errorInfo: React.ErrorInfo | null; 16 | } 17 | 18 | class ErrorBoundary extends React.Component< 19 | ErrorBoundaryProps, 20 | ErrorBoundaryState 21 | > { 22 | constructor(props: ErrorBoundaryProps) { 23 | super(props); 24 | this.state = { 25 | hasError: false, 26 | error: null, 27 | errorInfo: null, 28 | }; 29 | } 30 | 31 | static getDerivedStateFromError(error: Error): Partial { 32 | return { 33 | hasError: true, 34 | error, 35 | }; 36 | } 37 | 38 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 39 | this.setState({ 40 | error: error, 41 | errorInfo: errorInfo, 42 | }); 43 | } 44 | 45 | render() { 46 | if (this.state.hasError) { 47 | const Fallback = this.props.fallback; 48 | return ( 49 | this.setState({ hasError: false })} 53 | /> 54 | ); 55 | } 56 | 57 | return this.props.children; 58 | } 59 | } 60 | 61 | export default ErrorBoundary; 62 | -------------------------------------------------------------------------------- /docs/docs/hooks/useToggle.mdx: -------------------------------------------------------------------------------- 1 | # useToggle() 2 | 3 | The `useToggle()` hook can toggle between a set of two possible values and automatically update the state with the new value, if you want to toggle a boolean's state, see `useBoolToggle()` below. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useToggle } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseToggleDemo } from '../../demo/UseToggleDemo.jsx'; 14 | 15 | 16 | 17 | #### General 18 | 19 | ```jsx 20 | import { useToggle } from 'react-haiku'; 21 | 22 | export const Component = () => { 23 | const [theme, toggleTheme] = useToggle('dark', ['dark', 'light']); 24 | 25 | return ( 26 | <> 27 | {`Theme: ${theme}`} 28 | 29 | 30 | ); 31 | } 32 | ``` 33 | 34 | #### Boolean 35 | 36 | ```jsx 37 | import { useBoolToggle } from 'react-haiku'; 38 | 39 | export const Component = () => { 40 | const [isTrue, toggleTrue] = useBoolToggle(true) // default initial value is false 41 | 42 | return ( 43 | <> 44 | {`State: ${isTrue}`} 45 | 46 | 47 | ); 48 | } 49 | ``` 50 | 51 | ### API 52 | 53 | This hook accepts the following arguments: 54 | - `initialValue` - the initial preferred value for the toggle state 55 | - `options` - must be an array with strictly 2 elements and must also contain the value set for `initialValue` -------------------------------------------------------------------------------- /docs/demo/UseGeolocationDemo.jsx: -------------------------------------------------------------------------------- 1 | import { useGeolocation } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseGeolocationDemo = () => { 5 | const { latitude, longitude, error, loading } = useGeolocation({ 6 | enableHighAccuracy: true, 7 | timeout: 10000, 8 | }); 9 | 10 | return ( 11 |
12 | Geolocation Tracker! 13 | 14 | {loading && ( 15 |

16 | Loading location... 17 |

18 | )} 19 | 20 | {error && ( 21 |
22 |

Error: {error.message}

23 |

Code: {error.code}

24 |
25 | )} 26 | 27 | {!loading && !error && latitude !== null && longitude !== null && ( 28 |
29 |

30 | Latitude: {latitude.toFixed(6)} 31 |

32 |

33 | Longitude: {longitude.toFixed(6)} 34 |

35 |
36 | )} 37 | 38 | 39 | Note: You may need to allow location permission in your browser. 40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /docs/docs/hooks/useCookie.mdx: -------------------------------------------------------------------------------- 1 | # useCookie() 2 | 3 | The useCookie() hook provides reactive cookie management with automatic synchronization across browser tabs. Use this for persistent storage of simple data that needs to survive page reloads. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useCookie } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseCookieDemo } from '../../demo/UseCookieDemo.tsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useCookie } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const [userPref, setUserPref, deleteUserPref] = useCookie( 25 | 'theme_preference', 26 | 'light', 27 | 30, 28 | ); 29 | 30 | return ( 31 | <> 32 |

Current theme: {userPref}

33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | ``` 40 | 41 | ### API 42 | 43 | #### Arguments 44 | 45 | - `key` - Cookie identifier (required) 46 | - `initialValue` - Default value if cookie doesn't exist (required) 47 | - `expireDays` - Cookie expiration in days (optional, default 365) 48 | 49 | #### Returns 50 | 51 | - `cookieValue` - Current cookie value 52 | - `setValue` - Setter function (accepts new value) 53 | - `deleteValue` - Delete function (removes cookie) 54 | -------------------------------------------------------------------------------- /docs/src/components/Animations/SplitText.js: -------------------------------------------------------------------------------- 1 | import { useSprings, animated } from '@react-spring/web'; 2 | import React from 'react'; 3 | 4 | export const SplitText = ({ text, className = '', delay = 100 }) => { 5 | const letters = text.split(''); 6 | const [inView, setInView] = React.useState(false); 7 | const ref = React.useRef(); 8 | 9 | React.useEffect(() => { 10 | const observer = new IntersectionObserver( 11 | ([entry]) => { 12 | if (entry.isIntersecting) { 13 | setInView(true); 14 | observer.unobserve(ref.current); // Unobserve after triggering the animation 15 | } 16 | }, 17 | { threshold: 0.1, rootMargin: '-100px' } 18 | ); 19 | 20 | observer.observe(ref.current); 21 | 22 | return () => observer.disconnect(); 23 | }, []); 24 | 25 | const springs = useSprings( 26 | letters.length, 27 | letters.map((_, i) => ({ 28 | from: { opacity: 0, transform: 'translate3d(0,40px,0)' }, 29 | to: async (next) => { 30 | await next({ opacity: 1, transform: 'translate3d(0,0px,0)' }); 31 | await next({ opacity: 1, transform: 'translate3d(0,0,0)' }); 32 | }, 33 | delay: i * delay, 34 | })) 35 | ); 36 | 37 | return ( 38 |

39 | {springs.map((props, index) => ( 40 | 41 | {letters[index] === ' ' ? '\u00A0' : letters[index]} 42 | 43 | ))} 44 |

45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/hooks/useKeyPress.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef } from 'react'; 2 | 3 | const normalizeKey = (key: string) => { 4 | return key.toLowerCase(); 5 | }; 6 | 7 | export function useKeyPress( 8 | keys: string[], 9 | callback: (e: KeyboardEvent) => void, 10 | ) { 11 | const lastKeyPressed = useRef>(new Set([])); 12 | const keysSet = useMemo(() => { 13 | return new Set(keys.map((key) => normalizeKey(key))); 14 | }, [keys]); 15 | 16 | const handleKeyDown = (e: KeyboardEvent) => { 17 | if (e.repeat) return; // To prevent this function from triggering on key hold e.g. Ctrl hold 18 | 19 | lastKeyPressed.current?.add(normalizeKey(e.key)); 20 | 21 | // To bypass TypeScript check for the new ECMAScript method `isSubset` 22 | if ((keysSet as any).isSubsetOf(lastKeyPressed.current)) { 23 | e.preventDefault(); 24 | callback(e); 25 | } 26 | }; 27 | 28 | const handleKeyUp = (e: KeyboardEvent) => { 29 | lastKeyPressed.current?.delete(normalizeKey(e.key)); 30 | }; 31 | 32 | const handleBlur = () => { 33 | lastKeyPressed.current?.clear(); 34 | }; 35 | 36 | useEffect(() => { 37 | window.addEventListener('keydown', handleKeyDown); 38 | window.addEventListener('keyup', handleKeyUp); 39 | window.addEventListener('blur', handleBlur); 40 | 41 | return () => { 42 | window.removeEventListener('keydown', handleKeyDown); 43 | window.removeEventListener('keyup', handleKeyUp); 44 | window.removeEventListener('blur', handleBlur); 45 | }; 46 | }, [keysSet, callback]); 47 | } 48 | -------------------------------------------------------------------------------- /lib/hooks/useIntersectionObserver.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef } from "react"; 2 | 3 | type IntersectionObserverOptions = { 4 | threshold?: number | number[]; 5 | rootMargin?: string; 6 | }; 7 | 8 | type UseIntersectionObserverProps = { 9 | animateOnce?: boolean; 10 | options?: IntersectionObserverOptions; 11 | }; 12 | 13 | type IntersectionObserverResult = { 14 | observeRef: React.MutableRefObject; 15 | isVisible: boolean; 16 | } 17 | 18 | export const useIntersectionObserver = ({ 19 | animateOnce = false, 20 | options = {} 21 | }: UseIntersectionObserverProps = {}): IntersectionObserverResult => { 22 | const observeRef = useRef(null); 23 | const [isVisible, setIsVisible] = useState(false); 24 | 25 | if (typeof IntersectionObserver === "undefined") { 26 | console.warn("IntersectionObserver is not supported in this browser."); 27 | return { observeRef, isVisible: true }; 28 | } 29 | 30 | useEffect(() => { 31 | const currentRef = observeRef.current; 32 | if (!currentRef) return; 33 | 34 | const observer = new IntersectionObserver((entries) => { 35 | const entry = entries[0]; 36 | if (!entry) return; 37 | 38 | setIsVisible(entry.isIntersecting); 39 | 40 | if (entry.isIntersecting && animateOnce) { 41 | observer.disconnect(); 42 | } 43 | }, options); 44 | 45 | observer.observe(currentRef); 46 | 47 | return () => { 48 | observer.disconnect(); 49 | }; 50 | }, [observeRef, animateOnce, options]); 51 | 52 | return { observeRef, isVisible }; 53 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-haiku", 3 | "version": "2.4.1", 4 | "description": "React Hook & Utility Library", 5 | "sideEffects": false, 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "homepage": "https://reacthaiku.dev/", 10 | "author": "David Haz, Contributors @ github.com/DavidHDev/react-haiku", 11 | "license": "MIT", 12 | "scripts": { 13 | "publish": "npm publish --access-public", 14 | "test": "jest --watch --coverage", 15 | "build": "tsc" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/DavidHDev/react-haiku.git" 20 | }, 21 | "keywords": [ 22 | "reactjs", 23 | "react", 24 | "utility", 25 | "components", 26 | "javascript", 27 | "hooks", 28 | "state", 29 | "frontend", 30 | "react-hooks" 31 | ], 32 | "exports": { 33 | ".": { 34 | "import": "./dist/index.js", 35 | "require": "./dist/index.js", 36 | "types": "./dist/index.d.ts" 37 | } 38 | }, 39 | "peerDependencies": { 40 | "react": ">=16.8.0" 41 | }, 42 | "dependencies": { 43 | "react": ">=16.8.0" 44 | }, 45 | "devDependencies": { 46 | "@testing-library/jest-dom": "^6.4.8", 47 | "@testing-library/react": "^16.3.0", 48 | "@types/jest": "^29.5.12", 49 | "@types/node": "^20.12.2", 50 | "@types/react": "^18.2.14", 51 | "@types/react-dom": "^18.2.6", 52 | "jest": "^29.7.0", 53 | "jest-environment-jsdom": "^29.7.0", 54 | "ts-jest": "^29.2.4", 55 | "typescript": "^5.1.6" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/tests/hooks/useTitle.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useTitle } from '../../hooks/useTitle'; 3 | 4 | jest.mock('../../hooks/useIsomorphicLayoutEffect', () => ({ 5 | useIsomorphicLayoutEffect: jest.fn((callback) => { 6 | callback(); 7 | }) 8 | })); 9 | 10 | describe('useTitle', () => { 11 | const originalTitle = document.title; 12 | 13 | afterEach(() => { 14 | document.title = originalTitle; 15 | }); 16 | 17 | it('should set document title with valid string', () => { 18 | renderHook(() => useTitle('Test Title')); 19 | 20 | expect(document.title).toBe('Test Title'); 21 | }); 22 | 23 | it('should trim whitespace from title', () => { 24 | renderHook(() => useTitle(' Trimmed Title ')); 25 | 26 | expect(document.title).toBe('Trimmed Title'); 27 | }); 28 | 29 | it('should not set document title with empty string', () => { 30 | renderHook(() => useTitle('')); 31 | 32 | expect(document.title).toBe(originalTitle); 33 | }); 34 | 35 | it('should not set document title with whitespace-only string', () => { 36 | renderHook(() => useTitle(' ')); 37 | 38 | expect(document.title).toBe(originalTitle); 39 | }); 40 | 41 | it('should update title when title prop changes', () => { 42 | const { rerender } = renderHook(({ title }) => useTitle(title), { 43 | initialProps: { title: 'First Title' } 44 | }); 45 | 46 | expect(document.title).toBe('First Title'); 47 | 48 | rerender({ title: 'Second Title' }); 49 | 50 | expect(document.title).toBe('Second Title'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /docs/static/img/open-source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/hooks/useScript.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useScript(src: string) { 4 | const [status, setStatus] = useState(src ? 'loading' : 'idle'); 5 | 6 | useEffect(() => { 7 | if (!src) { 8 | setStatus('idle'); 9 | 10 | return; 11 | } 12 | 13 | let script: HTMLScriptElement | null = document.querySelector( 14 | `script[src="${src}"]`, 15 | ); 16 | 17 | if (!script) { 18 | script = document.createElement('script'); 19 | script.src = src; 20 | script.async = true; 21 | script.setAttribute('data-status', 'loading'); 22 | 23 | document.body.appendChild(script); 24 | 25 | const setAttributeFromEvent = (event: Event) => { 26 | script?.setAttribute( 27 | 'data-status', 28 | event.type === 'load' ? 'ready' : 'error', 29 | ); 30 | }; 31 | 32 | script.addEventListener('load', setAttributeFromEvent); 33 | script.addEventListener('error', setAttributeFromEvent); 34 | } else { 35 | setStatus(script.getAttribute('data-status')); 36 | } 37 | 38 | const setStateFromEvent = (event: Event) => 39 | setStatus(event.type === 'load' ? 'ready' : 'error'); 40 | 41 | script.addEventListener('load', setStateFromEvent); 42 | script.addEventListener('error', setStateFromEvent); 43 | 44 | return () => { 45 | if (script) { 46 | script.removeEventListener('load', setStateFromEvent); 47 | script.removeEventListener('error', setStateFromEvent); 48 | } 49 | }; 50 | }, [src]); 51 | 52 | return status; 53 | } 54 | -------------------------------------------------------------------------------- /docs/docs/hooks/useEventListener.mdx: -------------------------------------------------------------------------------- 1 | # useEventListener() 2 | 3 | The useEventListener() hook lets you quickly add an event to a certain `ref` or the app's `window` if no `ref` is specified 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useEventListener } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseEventListenerDemo } from '../../demo/UseEventListenerDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState, useRef } from 'react' 19 | import { useEventListener } from "react-haiku" 20 | 21 | export const Component = () => { 22 | const [countWindow, setCountWindow] = useState(0); 23 | const [countRef, setCountRef] = useState(0); 24 | 25 | // Button Ref 26 | const buttonRef = useRef(null) 27 | 28 | // Event Handlers 29 | const countW = () => setCountWindow(countWindow + 1); 30 | const countR = () => setCountRef(countRef + 1); 31 | 32 | // Example 1: Window Event 33 | useEventListener('scroll', countW); 34 | 35 | // Example 2: Element Event 36 | useEventListener('click', countR, buttonRef); 37 | 38 | return ( 39 | <> 40 | Window Event Triggered {countWindow} Times! 41 | Ref Event Triggered {countRef} Times! 42 | 43 | 44 | ); 45 | } 46 | ``` 47 | 48 | ### API 49 | 50 | This hook accepts the following arguments: 51 | - `eventName` - the event string value that you want to set to your ref/window 52 | - `handler` - the handler function for your event 53 | - `element` - the ref you can provide for targetting elements for your event -------------------------------------------------------------------------------- /docs/docs/utilities/classes.mdx: -------------------------------------------------------------------------------- 1 | # Classes 2 | 3 | The `Classes` component is a utility component that conditionally applies different sets of classes based on multiple independent conditions simultaneously. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { Classes } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { ClassesDemo } from '../../demo/ClassesDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import React, { useState } from 'react'; 19 | import { Classes } from 'react-haiku'; 20 | 21 | const Component = () => { 22 | const [hasError, setHasError] = useState(false); 23 | const [isSquared, setIsSquared] = useState(false); 24 | const [isDisabled, setIsDisabled] = useState(false); 25 | 26 | return ( 27 | 36 | ); 37 | }; 38 | 39 | export default Component; 40 | ``` 41 | 42 | ### API 43 | 44 | The component accepts the following props: 45 | 46 | - `className` - The initial class name for the element. Defaults to an empty string. 47 | - `toggleClasses` - An object with condition-to-classes mappings. Defaults to an empty object. 48 | - `children` - The content to be rendered inside the element. 49 | - `as (ElementType)`: The type of HTML element to render. Defaults to div. You can specify other elements like section, article, etc. 50 | - `[key: string]: any`: Any additional props to be passed to the element. 51 | -------------------------------------------------------------------------------- /lib/hooks/useHold.ts: -------------------------------------------------------------------------------- 1 | import { off, on } from '../helpers/event'; 2 | import { useCallback, useRef, type MouseEvent, type TouchEvent } from 'react'; 3 | 4 | type EventType = MouseEvent | TouchEvent; 5 | const isTouchEvent = (e: EventType): e is TouchEvent => 'touches' in e; 6 | 7 | const preventDefault = (e: EventType) => { 8 | if (!isTouchEvent(e)) { 9 | return; 10 | } 11 | 12 | if (e.touches.length < 2 && e.preventDefault) { 13 | e.preventDefault(); 14 | } 15 | }; 16 | 17 | export const useHold = ( 18 | callback: (e: EventType) => any, 19 | { doPreventDefault = true, delay = 1000 } = {}, 20 | ) => { 21 | const timeout = useRef(); 22 | const target = useRef(); 23 | 24 | const start = useCallback( 25 | (event: EventType) => { 26 | if (doPreventDefault && event.target) { 27 | on(event.target, 'touchend', preventDefault, { passive: false }); 28 | target.current = event.target; 29 | } 30 | 31 | timeout.current = setTimeout( 32 | () => callback(event), 33 | delay, 34 | ) as unknown as number; 35 | }, 36 | [callback, delay, doPreventDefault], 37 | ); 38 | 39 | const clear = useCallback(() => { 40 | timeout.current && clearTimeout(timeout.current); 41 | 42 | if (doPreventDefault && target.current) { 43 | off(target.current, 'touchend', preventDefault); 44 | } 45 | }, [doPreventDefault]); 46 | 47 | return { 48 | onMouseDown: (e: MouseEvent) => start(e), 49 | onTouchStart: (e: TouchEvent) => start(e), 50 | onMouseUp: clear, 51 | onMouseLeave: clear, 52 | onTouchEnd: clear, 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /docs/demo/UseScrollDeviceDemo.tsx: -------------------------------------------------------------------------------- 1 | import { useScrollDevice } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | export const UseScrollDeviceDemo = () => { 5 | const device = useScrollDevice(); 6 | 7 | return ( 8 |
9 |

Scroll Detection:

10 | 11 |
19 | {device?.toUpperCase() || 'SCROLL TO DETECT'} 20 |
21 | 22 |
32 |
33 |
34 | Mouse Wheel 35 |
36 | 37 |
38 |
41 | Trackpad 42 |
43 |
44 | 45 |

53 | Scroll vertically to detect input device 54 |

55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /docs/docs/utilities/show.mdx: -------------------------------------------------------------------------------- 1 | # Show 2 | 3 | The `Show` component can be used for complex conditional rendering. The component can be extended by multiple `When` components and an `Else` component to render their contents based on the conditions you provide. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { Show } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { ShowDemo } from '../../demo/ShowDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import { useState } from 'react'; 19 | import { Show } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const [number, setNumber] = useState(6); 23 | 24 | return ( 25 | 26 | 27 | Number is 6! 28 | 29 | 30 | 31 | 32 | Number is 7! 33 | 34 | 35 | 36 | 37 | No valid number found! 38 | 39 | 40 | 41 | ); 42 | } 43 | ``` 44 | 45 | ### API 46 | 47 | The `Show` component accepts two types of child components: 48 | 49 | - `When` - this component can be used multiple times. It renders its contents when the condition passed inside its `isTrue` prop is evaluated as truthy 50 | - `Else` - this component can only be used once and will render its contents when none of the `When` components have met their conditions. -------------------------------------------------------------------------------- /docs/demo/UseWebSocketDemo.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useWebSocket } from 'react-haiku'; 3 | 4 | export const UseWebSocketDemo = () => { 5 | const [newValue, setNewValue] = useState(''); 6 | 7 | const { status, lastMessage, sendMessage } = useWebSocket( 8 | 'wss://echo.websocket.org', 9 | ); 10 | 11 | useEffect(() => { 12 | if (status === 'OPEN') { 13 | sendMessage('useWebSocket() with Haiku!'); 14 | } 15 | }, [status]); 16 | 17 | return ( 18 |
19 |

20 | Web Socket Status: {status} 21 |

22 |
23 | setNewValue(e.target.value)} 30 | /> 31 | 37 |
38 |
44 | Last Message: 45 |

46 | {' '} 47 | 48 | {lastMessage ? lastMessage : 'No message yet'} 49 | 50 |

51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /docs/docs/hooks/useIdle.mdx: -------------------------------------------------------------------------------- 1 | # useIdle() 2 | 3 | The `useIdle()` hook lets you detect current user activity or inactivity on a web page, returning a boolean value that represents whether or not the user is currently active. The user is set as inactive when no events are triggered after a specified delay. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useIdle } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | #### Default Options 14 | 15 | import { UseIdleDemo } from '../../demo/UseIdleDemo.jsx'; 16 | 17 | 18 | 19 | 20 | ```jsx 21 | import { useIdle } from "react-haiku" 22 | 23 | export const Component = () => { 24 | const idle = useIdle(3000); 25 | 26 | return Current Status: {idle ? 'Idle' : 'Active'} 27 | } 28 | ``` 29 | 30 | #### Custom Options 31 | 32 | import { UseIdleCustomDemo } from '../../demo/UseIdleDemo.jsx'; 33 | 34 | 35 | 36 | 37 | ```jsx 38 | import { useIdle } from "react-haiku" 39 | 40 | export const Component = () => { 41 | const idle = useIdle(1000, { events: ['click', 'touchstart'], initialState: false }); 42 | 43 | return ( 44 | <> 45 | Works only with click/touch events! 46 | Current Status: {idle ? 'Idle' : 'Active'} 47 | 48 | ); 49 | } 50 | ``` 51 | 52 | 53 | ### API 54 | 55 | This hook accepts the following arguments: 56 | 57 | - `timeout` - the time until the state is set as inactive 58 | - `options` - the options object you can pass in 59 | - `events` - string array, the collection of events that will trigger activity signals, by default: `['keypress', 'mousemove', 'touchmove', 'click', 'scroll']` 60 | - `initialState` - boolean, the initial activity state -------------------------------------------------------------------------------- /docs/docs/hooks/useFullscreen.mdx: -------------------------------------------------------------------------------- 1 | # useFullscreen() 2 | 3 | The `useFullscreen()` hook can toggle between entering fullscreen mode and exiting fullscreen mode. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useFullscreen } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UseFullscreenDemo } from '../../demo/UseFullscreenDemo.jsx'; 14 | 15 | 16 | 17 | #### For Overall Document 18 | 19 | ```jsx 20 | 21 | import { useEffect, useRef } from 'react' 22 | import { useFullscreen } from 'react-haiku'; 23 | 24 | export const Component = () => { 25 | const documentRef = useRef(null); 26 | 27 | useEffect(() => { 28 | documentRef.current = document.documentElement; 29 | }, []); 30 | 31 | const {isFullscreen, toggleFullscreen } = useFullscreen(documentRef); 32 | return ( 33 |
34 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"} 35 | 36 |
37 | ); 38 | } 39 | 40 | ``` 41 | 42 | #### For Specific Element 43 | 44 | ```jsx 45 | 46 | import { useEffect } from 'react' 47 | import { useFullscreen } from 'react-haiku'; 48 | 49 | export const Component = () => { 50 | const elementRef = useRef(null); 51 | const {isFullscreen, toggleFullscreen } = useFullscreen(elementRef); 52 | return ( 53 |
54 | Is in Fullscreen Mode: {isFullscreen ? "True" : "False"} 55 | 56 |
57 | ); 58 | } 59 | 60 | ``` 61 | 62 | ### API 63 | 64 | This hook accepts the following arguments: 65 | - `targetRef` - a reference to the DOM element you want to toggle fullscreen for. -------------------------------------------------------------------------------- /docs/docs/hooks/useTImer.mdx: -------------------------------------------------------------------------------- 1 | # useTimer() 2 | 3 | The `useTimer` hook provides a simple way to manage a timer with start, pause, and reset functionalities. It supports both counting up and counting down with a customizable interval. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useTimer } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseTimerDemo } from '../../demo/UseTimerDemo.jsx'; 15 | 16 | Loading...
}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useTimer } from 'react-haiku'; 22 | export const TimerComponent = () => { 23 | const { time, isRunning, start, pause, reset } = useTimer({ 24 | startTime: 0, 25 | endTime: 10, 26 | interval: 1000, 27 | }); 28 | 29 | return ( 30 |
31 |

Time: {time}

32 | 35 | 38 | 39 |
40 | ); 41 | }; 42 | ``` 43 | 44 | ### API 45 | 46 | This hook accepts the following arguments: 47 | 48 | - `startTime` - the initial time value for the timer 49 | - `endTime` - the end time value for the timer 50 | - `interval` (number, default: `1000`)- the interval in milliseconds to update the timer value 51 | 52 | This hook returns an object with the following properties: 53 | 54 | - `time` - the current time value of the timer 55 | - `isRunning` - a boolean value indicating whether the timer is running 56 | - `start` - a function to start the timer 57 | - `pause` - a function to pause the timer 58 | - `reset` - a function to reset the timer 59 | -------------------------------------------------------------------------------- /docs/docs/hooks/useIntersectionObserver.mdx: -------------------------------------------------------------------------------- 1 | # useIntersectionObserver() 2 | 3 | The `useIntersectionObserver` hook provides a way to detect when an element enters or exits the viewport. It offers options for configuring intersection thresholds, margins, and one-time animation triggers. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useIntersectionObserver } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseIntersectionObserverDemo } from '../../demo/UseIntersectionObserverDemo.jsx'; 15 | 16 | Loading...
}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | 22 | import { useIntersectionObserver } from 'react-haiku'; 23 | 24 | export const Component = () => { 25 | const {observeRef, isVisible} = useIntersectionObserver({ 26 | animateOnce: false, 27 | options:{ 28 | threshold: .5, 29 | rootMargin: '-40% 0px -40% 0px' 30 | } 31 | }) 32 | return ( 33 |
34 |

35 | I'm being observed! 36 |

37 |
38 | ); 39 | } 40 | 41 | ``` 42 | 43 | ### API 44 | 45 | This hook accepts the following arguments: 46 | 47 | - `animateOnce` (optional) - boolean indicating whether the element should be observed only once (`true`) or continuously (`false`), the default value for this argument is `false`. 48 | - `options` - an object containing IntersectionObserver options: 49 | - `threshold` (optional) - a single number or an array of numbers representing the percentage of the target's visibility the observer's callback should trigger. 50 | - `rootMargin` (optional) - a string which defines a margin around the root. This can be used to grow or shrink the area used for intersection detection. -------------------------------------------------------------------------------- /docs/docs/hooks/usePermission.mdx: -------------------------------------------------------------------------------- 1 | # usePermission() 2 | 3 | The `usePermission()` check browser permissions for querying state for various browser APIs 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { usePermission } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { UsePermissionDemo } from '../../demo/UsePermissionDemo.tsx'; 14 | 15 | 16 | 17 | ```tsx 18 | import { useState } from 'react'; 19 | import { usePermission } from 'react-haiku'; 20 | 21 | export const Component = () => { 22 | const state = usePermission("geolocation") 23 | const [location, setLocation] = useState(null) 24 | 25 | function handleGetCurrentPosition() { 26 | if (state !== "prompt" && state !== "granted") return 27 | 28 | navigator.geolocation.getCurrentPosition((location) => { 29 | setLocation(location) 30 | }) 31 | } 32 | 33 | return ( 34 |
35 |

Permission state: {state}

36 | 37 |
38 |         {JSON.stringify(location ?? {}, null, 2)}
39 |       
40 | 41 |
42 | 45 |
46 |
47 | ); 48 | } 49 | ``` 50 | 51 | ### API 52 | 53 | #### Arguments 54 | 55 | - `permission` - The name of the API whose permissions you want to query, such as the `PermissionName` and other described in the [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Permissions/query#name) documentation. 56 | 57 | #### Returns 58 | 59 | The the state of a requested permission, combining the standard `PermissionState` with additional internal states. 60 | - `checking`: The permission state is being verified. 61 | - `not-supported`: The permission check is not supported or an error occurred during verification. 62 | -------------------------------------------------------------------------------- /docs/docs/utilities/class.mdx: -------------------------------------------------------------------------------- 1 | # Class 2 | 3 | The `Class` component is a utility component that conditionally applies a CSS class to a `div` element based on a boolean condition. It is useful for toggling styles dynamically. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { Class } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { ClassDemo } from '../../demo/ClassDemo.jsx'; 14 | 15 | 16 | 17 | ```jsx 18 | import React, { useState } from 'react'; 19 | import { Class } from 'react-haiku'; 20 | 21 | const Component = () => { 22 | const [isActive, setIsActive] = useState(false); 23 | 24 | const toggleActive = () => { 25 | setIsActive(!isActive); 26 | }; 27 | 28 | return ( 29 |
30 | 33 | 34 | 40 | This is a box that will toggle its class based on the button click. 41 | 42 |
43 | ); 44 | }; 45 | 46 | export default Component; 47 | ``` 48 | 49 | ### API 50 | 51 | The component accepts the following props: 52 | 53 | - `className` - The initial class name for the `div` element. Defaults to an empty string. 54 | - `condition` - The condition to determine whether to apply the `toggleClass` or not. Defaults to `false`. 55 | - `toggleClass` - The class name to be toggled based on the condition. Defaults to an empty string. 56 | - `children` - The content to be rendered inside the `div` element. 57 | - `as (ElementType)`: The type of HTML element to render. Defaults to div. You can specify other elements like section, article, etc. 58 | - `[key: string]: any`: Any additional props to be passed to the element. 59 | -------------------------------------------------------------------------------- /lib/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react'; 2 | import { useEventListener } from './useEventListener'; 3 | 4 | export function useLocalStorage(key: string, initialValue: T) { 5 | const readValue = useCallback(() => { 6 | if (typeof window === 'undefined') { 7 | return initialValue; 8 | } 9 | 10 | try { 11 | const item = window.localStorage.getItem(key); 12 | 13 | return item ? parseJSON(item) : initialValue; 14 | } catch (error) { 15 | console.error(`Error getting storage key “${key}”:`, error); 16 | 17 | return initialValue; 18 | } 19 | }, [initialValue, key]); 20 | 21 | const [storedValue, setStoredValue] = useState(readValue); 22 | const setValueRef = useRef(); 23 | 24 | setValueRef.current = (value: T) => { 25 | try { 26 | const newValue = value instanceof Function ? value(storedValue) : value; 27 | window.localStorage.setItem(key, JSON.stringify(newValue)); 28 | 29 | setStoredValue(newValue); 30 | window.dispatchEvent(new Event('local-storage')); 31 | } catch (error) { 32 | console.warn(`Error adding "${key}" to storage:`, error); 33 | } 34 | }; 35 | 36 | const setValue = useCallback((value: T) => setValueRef.current?.(value), []); 37 | 38 | useEffect(() => { 39 | setStoredValue(readValue()); 40 | }, []); 41 | 42 | const handleStorageChange = useCallback( 43 | () => setStoredValue(readValue()), 44 | [readValue], 45 | ); 46 | useEventListener('storage', handleStorageChange); 47 | useEventListener('local-storage', handleStorageChange); 48 | return [storedValue, setValue]; 49 | } 50 | 51 | function parseJSON(value: any) { 52 | try { 53 | return value === 'undefined' ? undefined : JSON.parse(value ?? ''); 54 | } catch { 55 | return undefined; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/hooks/useScrollDevice.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | type ScrollDevice = "mouse" | "trackpad" | null; 4 | 5 | export function useScrollDevice(): ScrollDevice { 6 | const [deviceType, setDeviceType] = useState(null); 7 | let debounceTimeout: ReturnType | null = null; 8 | 9 | useEffect(() => { 10 | let lastEventTimestamp = 0; 11 | let lastDeltaY = 0; 12 | 13 | const detectScrollDevice = (event: WheelEvent) => { 14 | const now = performance.now(); 15 | const timeDiff = now - lastEventTimestamp; 16 | 17 | const isTrackpad = ( 18 | Math.abs(event.deltaY) < 50 || // Small delta values indicate trackpad 19 | (Math.abs(event.deltaX) > 0 && Math.abs(event.deltaY) < 50) // Trackpads scroll in both directions 20 | ); 21 | 22 | const isMouseWheel = ( 23 | Math.abs(event.deltaY) >= 50 || // Large jumps indicate mouse wheel 24 | (timeDiff > 50 && Math.abs(event.deltaY - lastDeltaY) < 10) // Consistent jumps suggest a wheel 25 | ); 26 | 27 | const detectedDevice = isTrackpad ? "trackpad" : isMouseWheel ? "mouse" : null; 28 | 29 | lastEventTimestamp = now; 30 | lastDeltaY = event.deltaY; 31 | 32 | // Debounce: Ensure only the last detected value is stored 33 | if (debounceTimeout) { 34 | clearTimeout(debounceTimeout); 35 | } 36 | 37 | debounceTimeout = setTimeout(() => { 38 | setDeviceType(detectedDevice); 39 | }, 200); // Adjust debounce time if needed 40 | }; 41 | 42 | window.addEventListener("wheel", detectScrollDevice); 43 | 44 | return () => { 45 | window.removeEventListener("wheel", detectScrollDevice); 46 | if (debounceTimeout) { 47 | clearTimeout(debounceTimeout); 48 | } 49 | }; 50 | }, []); 51 | 52 | return deviceType; 53 | } 54 | -------------------------------------------------------------------------------- /lib/hooks/useScreenSize.ts: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useLayoutEffect } from 'react'; 2 | 3 | const breakpoints = { 4 | xs: 639, 5 | sm: 767, 6 | md: 1023, 7 | lg: 1279, 8 | xl: 1535, 9 | '2xl': Infinity, 10 | }; 11 | 12 | const getBreakpoint = (width: number): keyof typeof breakpoints => { 13 | return (Object.keys(breakpoints) as (keyof typeof breakpoints)[]) 14 | .find(bp => width <= breakpoints[bp]) ?? '2xl'; 15 | }; 16 | 17 | export const useScreenSize = () => { 18 | const [screenSize, setScreenSize] = useState(() => 19 | typeof window === 'undefined' ? 'xl' : getBreakpoint(window.innerWidth), 20 | ); 21 | 22 | const handleResize = useCallback(() => { 23 | setScreenSize(getBreakpoint(window.innerWidth)); 24 | }, []); 25 | 26 | useLayoutEffect(() => { 27 | if (typeof window === 'undefined') return; 28 | 29 | let ticking = false; 30 | 31 | const onResize = () => { 32 | if (ticking) return; 33 | ticking = true; 34 | requestAnimationFrame(() => { 35 | handleResize(); 36 | ticking = false; 37 | }); 38 | }; 39 | 40 | // Initial measurement 41 | handleResize(); 42 | window.addEventListener('resize', onResize); 43 | 44 | return () => window.removeEventListener('resize', onResize); 45 | }, [handleResize]); 46 | 47 | const eq = (bp: keyof typeof breakpoints) => screenSize === bp; 48 | const lt = (bp: keyof typeof breakpoints) => breakpoints[screenSize] < breakpoints[bp]; 49 | const gt = (bp: keyof typeof breakpoints) => breakpoints[screenSize] > breakpoints[bp]; 50 | const lte = (bp: keyof typeof breakpoints) => eq(bp) || lt(bp); 51 | const gte = (bp: keyof typeof breakpoints) => eq(bp) || gt(bp); 52 | 53 | 54 | return { 55 | eq, 56 | lt, 57 | gt, 58 | lte, 59 | gte, 60 | toString: () => screenSize, 61 | }; 62 | }; 63 | -------------------------------------------------------------------------------- /docs/demo/UseCookieDemo.tsx: -------------------------------------------------------------------------------- 1 | import { useCookie } from 'react-haiku'; 2 | import React, { useState } from 'react'; 3 | 4 | export const UseCookieDemo = () => { 5 | const [newValue, setNewValue] = useState(''); 6 | 7 | const [cookieValue, setCookie, deleteCookie] = useCookie( 8 | 'demo_cookie', 9 | 'default', 10 | 7, 11 | ) as [string, (value: string) => void, () => void]; 12 | 13 | return ( 14 |
15 |

Cookie Value:

16 |
{cookieValue}
17 | 18 |
19 | setNewValue(e.target.value)} 26 | /> 27 | 28 | 34 |
35 | 36 |
37 | 43 | 44 | 47 |
48 | 49 |

57 | Open DevTools → Application → Cookies to see persistence 58 |

59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /docs/docs/hooks/useGeolocation.mdx: -------------------------------------------------------------------------------- 1 | # useGeolocation() 2 | 3 | The `useGeolocation()` hook provides access to the user's current geographical location using the browser's Geolocation API. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useGeolocation } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseGeolocationDemo } from '../../demo/UseGeolocationDemo.jsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useGeolocation } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const { latitude, longitude, error, loading } = useGeolocation({ 25 | enableHighAccuracy: true, 26 | timeout: 10000, 27 | }); 28 | 29 | if (loading) { 30 | return
Getting your location...
; 31 | } 32 | 33 | if (error) { 34 | return
Error: {error.message}
; 35 | } 36 | 37 | return ( 38 |
39 |

Latitude: {latitude}

40 |

Longitude: {longitude}

41 |
42 | ); 43 | }; 44 | ``` 45 | 46 | ### API 47 | 48 | This hook accepts an optional configuration object with the following properties: 49 | 50 | - `enableHighAccuracy` - boolean to request the most accurate results possible, defaults to `false` 51 | - `timeout` - maximum time in milliseconds to wait before timeout, defaults to `5000` 52 | - `maximumAge` - maximum age of cached position in milliseconds, defaults to `0` 53 | - `watch` - boolean to continuously watch for position changes, defaults to `false` 54 | 55 | The hook returns an object with: 56 | 57 | - `latitude` - the latitude coordinate or `null` 58 | - `longitude` - the longitude coordinate or `null` 59 | - `error` - error object with `code` and `message` properties, or `null` 60 | - `loading` - boolean indicating if the request is in progress 61 | -------------------------------------------------------------------------------- /lib/hooks/useBatteryStatus.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | // Define BatteryManager type manually 4 | interface BatteryManager extends EventTarget { 5 | charging: boolean; 6 | level: number; 7 | addEventListener( 8 | type: 'chargingchange' | 'levelchange', 9 | listener: (this: BatteryManager, ev: Event) => any 10 | ): void; 11 | removeEventListener( 12 | type: 'chargingchange' | 'levelchange', 13 | listener: (this: BatteryManager, ev: Event) => any 14 | ): void; 15 | } 16 | 17 | type NavigatorWithBattery = Navigator & { 18 | getBattery?: () => Promise; 19 | }; 20 | 21 | export function useBatteryStatus() { 22 | const [batteryStatus, setBatteryStatus] = useState({ 23 | level: 0, 24 | isCharging: false, 25 | }); 26 | 27 | useEffect(() => { 28 | const navigatorWithBattery = navigator as NavigatorWithBattery; 29 | 30 | if (!navigatorWithBattery.getBattery) { 31 | console.warn("Battery Status API is not supported in this browser."); 32 | return; 33 | } 34 | 35 | let battery: BatteryManager | null = null; 36 | 37 | const updateBatteryStatus = () => { 38 | if (battery) { 39 | setBatteryStatus({ 40 | level: Math.round(battery.level * 100), 41 | isCharging: battery.charging, 42 | }); 43 | } 44 | }; 45 | 46 | navigatorWithBattery.getBattery().then((bat) => { 47 | battery = bat; 48 | updateBatteryStatus(); 49 | 50 | battery.addEventListener('chargingchange', updateBatteryStatus); 51 | battery.addEventListener('levelchange', updateBatteryStatus); 52 | }); 53 | 54 | return () => { 55 | if (battery) { 56 | battery.removeEventListener('chargingchange', updateBatteryStatus); 57 | battery.removeEventListener('levelchange', updateBatteryStatus); 58 | } 59 | }; 60 | }, []); 61 | 62 | return batteryStatus; 63 | } 64 | -------------------------------------------------------------------------------- /lib/tests/utils/Classes.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import { Classes } from '../../utils/Classes'; 3 | 4 | describe('Classes component', () => { 5 | const text = 'Classes Block'; 6 | 7 | it('renders the Classes component', () => { 8 | const { asFragment } = render( 9 | {text}, 10 | ); 11 | expect(asFragment()).toMatchSnapshot(); 12 | expect(screen.getByText(text)).toBeInTheDocument(); 13 | }); 14 | 15 | it('renders with base className', () => { 16 | render({text}); 17 | 18 | const element = screen.queryByText(text); 19 | expect(element).toHaveClass('base-class'); 20 | }); 21 | 22 | it('applies toggleClasses when condition is true', () => { 23 | render( 24 | 29 | {text} 30 | , 31 | ); 32 | const element = screen.getByText(text); 33 | expect(element).toHaveClass('base-class active-class'); 34 | }); 35 | 36 | it('does not apply toggleClasses when condition is false', () => { 37 | render( 38 | 42 | {text} 43 | , 44 | ); 45 | const element = screen.getByText(text); 46 | expect(element).not.toHaveClass('inactive-class'); 47 | }); 48 | 49 | it('removes duplicate classes', () => { 50 | render( 51 | 55 | {text} 56 | , 57 | ); 58 | const element = screen.getByText(text); 59 | expect(element).toHaveClass('base-class active-class focus'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /docs/demo/ClassesDemo.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Classes } from 'react-haiku'; 3 | 4 | export const ClassesDemo = () => { 5 | const [hasError, setHasError] = useState(false); 6 | const [isSquared, setIsSquared] = useState(false); 7 | const [isDisabled, setIsDisabled] = useState(false); 8 | 9 | return ( 10 |
11 |
19 |
20 | 21 | setHasError(!hasError)} 26 | /> 27 |
28 | 29 |
30 | 31 | setIsSquared(!isSquared)} 36 | /> 37 |
38 | 39 |
40 | 41 | setIsDisabled(!isDisabled)} 46 | /> 47 |
48 |
49 | 50 | 60 |
61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /docs/static/img/install-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/helpers/cookie.ts: -------------------------------------------------------------------------------- 1 | export const parseToDataType = ( 2 | value: string | undefined, 3 | isItRetry = false, 4 | ): T | undefined => { 5 | try { 6 | return value === "undefined" || value === undefined 7 | ? undefined 8 | : (JSON.parse(value) as T); 9 | } catch (e) { 10 | if (!isItRetry) { 11 | return parseToDataType(`"${value?.replaceAll?.('"', "")}"`, true); 12 | } 13 | 14 | return undefined; 15 | } 16 | }; 17 | 18 | export const parseToCookieType = (value: T) => { 19 | if (typeof value === "string") { 20 | return value; 21 | } 22 | 23 | return JSON.stringify(value); 24 | }; 25 | 26 | export const getCookie = (name: string): T | undefined => { 27 | const value = `; ${document.cookie}`; 28 | 29 | const [_, cookie] = value.split(`; ${name}=`); 30 | 31 | return cookie ? parseToDataType(cookie.split(";")[0]) : undefined; 32 | }; 33 | 34 | export const getCookies = = Record>( 35 | cookies: string[] = [], 36 | ): T => { 37 | if (cookies.length) { 38 | return cookies.reduce>( 39 | (result, cookie) => ({ 40 | ...result, 41 | [cookie]: getCookie(cookie), 42 | }), 43 | {}, 44 | ) as T; 45 | } 46 | 47 | return Object.fromEntries( 48 | document.cookie.split("; ").map((c) => { 49 | const [key, value] = c.split("="); 50 | return [key, parseToDataType(value)]; 51 | }), 52 | ) as T; 53 | }; 54 | 55 | export const setCookie = (name: string, value: T, expireDays: number) => { 56 | const date = new Date(); 57 | const millisecondsInADay = 24 * 60 * 60 * 1000; 58 | const expireDate = date.getTime() + expireDays * millisecondsInADay; 59 | 60 | date.setTime(expireDate); 61 | 62 | const expires = `expires=${date.toUTCString()}`; 63 | 64 | document.cookie = `${name}=${parseToCookieType(value)}; ${expires}; path=/;`; 65 | }; 66 | 67 | export const deleteCookie = (name: string) => { 68 | document.cookie = `${name}=; path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`; 69 | }; 70 | 71 | 72 | -------------------------------------------------------------------------------- /docs/docs/hooks/useTabNotification.mdx: -------------------------------------------------------------------------------- 1 | # useTabNotification() 2 | 3 | The useTabNotification() hook manages browser tab notifications through title modifications and favicon indicators. Use this to alert users of background activity or new notifications when they're in another tab. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { useTabNotification } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import BrowserOnly from '@docusaurus/BrowserOnly'; 14 | import { UseTabNotificationDemo } from '../../demo/UseTabNotificationDemo.tsx'; 15 | 16 | Loading...}> 17 | {() => } 18 | 19 | 20 | ```jsx 21 | import { useTabNotification } from 'react-haiku'; 22 | 23 | export const Component = () => { 24 | const { 25 | setTitlePrefix, 26 | setFlashMessage, 27 | setIsShown, 28 | setCustomTitle, 29 | setShowFaviconDot, 30 | setFaviconDotColor, 31 | } = useTabNotification(); 32 | 33 | return ( 34 | <> 35 | 36 | 37 | setCustomTitle(e.target.value)} 41 | /> 42 | 43 | 52 | 53 | ); 54 | }; 55 | ``` 56 | 57 | ### API 58 | 59 | #### Arguments 60 | 61 | - `flashDelayInSeconds` - Alternate interval for title flashing (optional, default 2) 62 | 63 | #### Returns 64 | 65 | - `setTitlePrefix` - Adds persistent prefix to page title 66 | - `setFlashMessage` - Sets message to alternate with original title 67 | - `setIsShown` - Toggles notification visibility 68 | - `setCustomTitle` - Overrides default page title 69 | - `setShowFaviconDot` - Toggles favicon indicator 70 | - `setFaviconDotColor` - Sets favicon dot color (hex format) 71 | -------------------------------------------------------------------------------- /lib/utils/Classes.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, ElementType } from 'react'; 2 | 3 | // Check if a value is a plain object literal 4 | const isObjectLiteral = (value: any): boolean => { 5 | return Object.prototype.toString.call(value) === '[object Object]'; 6 | }; 7 | 8 | // Remove duplicate class names from a string 9 | const deduplicateClasses = (classes: string) => 10 | [...new Set(classes.split(' '))].join(' '); 11 | 12 | interface ClassProps { 13 | className?: string; 14 | toggleClasses?: Record; 15 | children: ReactNode; 16 | as?: ElementType; 17 | [key: string]: any; 18 | } 19 | 20 | /** 21 | * Classes component that conditionally applies class names to a specified HTML element. 22 | * 23 | * @param className - The initial class name for the element. 24 | * @param toggleClasses - An object with condition-to-classes mappings. 25 | * @param children - The content to be rendered inside the specified HTML element. 26 | * @param as - The type of HTML element to render. 27 | * @param props - Any additional props to be passed to the element. 28 | * @returns The rendered Class component. 29 | */ 30 | 31 | export const Classes: React.FC = ({ 32 | className = '', 33 | toggleClasses = {}, 34 | children, 35 | as: Component = 'div', 36 | ...props 37 | }) => { 38 | if (!isObjectLiteral(toggleClasses)) { 39 | console.error( 40 | 'toggleClasses prop must be an object literal. Received:', 41 | toggleClasses, 42 | ); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | 51 | let computedClassNames = className; 52 | 53 | for (const [classes, condition] of Object.entries(toggleClasses)) { 54 | if (condition) { 55 | computedClassNames += ` ${classes.trim()}`; 56 | } 57 | } 58 | 59 | computedClassNames = deduplicateClasses(computedClassNames); 60 | 61 | return ( 62 | 63 | {children} 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /docs/docs/utilities/errorBoundary.mdx: -------------------------------------------------------------------------------- 1 | # Error Boundary 2 | 3 | The `ErrorBoundary` component is a utility component that catches errors in its child component tree, logs the error, and prevents the entire application from crashing by rendering a fallback UI instead.. 4 | 5 | ### Import 6 | 7 | ```jsx 8 | import { ErrorBoundary } from 'react-haiku'; 9 | ``` 10 | 11 | ### Usage 12 | 13 | import { ErrorBoundaryDemo } from '../../demo/ErrorBoundaryDemo.tsx'; 14 | 15 | 16 | 17 | ```tsx 18 | import React from 'react'; 19 | import { ErrorBoundary } from 'react-haiku'; 20 | 21 | // Component that will intentionally cause an error 22 | const CrashComponent: React.FC = () => { 23 | throw new Error('This is a test error inside the ErrorBoundary!'); 24 | }; 25 | 26 | export const ErrorBoundaryDemo: React.FC = () => { 27 | return ( 28 | 29 | 30 | 31 | ); 32 | }; 33 | ``` 34 | 35 | ### Fallback 36 | 37 | You can access `retry`, `error` and `errorInfo` prop in you Fallback component which you can use to create a dynamic UI. 38 | 39 | ```tsx 40 | interface FallbackProps { 41 | retry: () => void; 42 | error: Error | null; 43 | errorInfo: React.ErrorInfo | null; 44 | } 45 | 46 | const Fallback: React.FC = ({ retry }) => { 47 | return ( 48 |
49 | We faced an error 50 | 57 |
58 | ); 59 | }; 60 | ``` 61 | 62 | ### API 63 | 64 | The **ErrorBoundary** accepts the following props: 65 | 66 | - `fallback` - Component to render when an error is occured in child components. 67 | 68 | The **Fallback** receives the following props: 69 | 70 | - `retry` - Function to retry/reload the wrapped component. 71 | - `error` - error of type **Error** 72 | - `errorInfo` - error info of type **React.ErrorInfo** 73 | -------------------------------------------------------------------------------- /docs/demo/SwitchDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from 'react-haiku'; 2 | import React from 'react'; 3 | 4 | enum Reaction { 5 | LIKE = 'like', 6 | FIRE = 'fire', 7 | LOVE = 'love', 8 | } 9 | 10 | const CaseReactionLike = () =>

👍

11 | const CaseReactionFire = () =>

🔥

12 | const CaseReactionLove = () =>

❤️

13 | const CaseDefault = () =>

🚀

14 | 15 | export const SwitchDemo = () => { 16 | const [reaction, setReaction] = React.useState(); 17 | 18 | const handleReact = (e: React.ChangeEvent) => { 19 | const reactionSelected = e.target.value as Reaction; 20 | setReaction({ 21 | [Reaction.LIKE]: Reaction.LIKE, 22 | [Reaction.FIRE]: Reaction.FIRE, 23 | [Reaction.LOVE]: Reaction.LOVE 24 | }[reactionSelected]); 25 | } 26 | 27 | return ( 28 |
29 |
30 | 31 |
32 | 38 |
39 |
40 | 41 | 50 |
51 | ); 52 | }; -------------------------------------------------------------------------------- /lib/tests/hooks/useToggle.test.tsx: -------------------------------------------------------------------------------- 1 | import { act, renderHook } from '@testing-library/react'; 2 | import { useBoolToggle, useToggle } from '../../hooks/useToggle'; 3 | 4 | describe('useToggle', () => { 5 | it('should initialize with the initial value', () => { 6 | const { result } = renderHook(() => useToggle('on', ['on', 'off'])); 7 | 8 | expect(result.current[0]).toBe('on'); 9 | expect(typeof result.current[1]).toBe('function'); 10 | }); 11 | 12 | it('should toggle between options when called without arguments', () => { 13 | const { result } = renderHook(() => useToggle('on', ['on', 'off'])); 14 | 15 | const toggleFn = result.current[1] as () => void; 16 | 17 | act(() => { 18 | toggleFn(); 19 | }); 20 | 21 | expect(result.current[0]).toBe('off'); 22 | 23 | act(() => { 24 | toggleFn(); 25 | }); 26 | 27 | expect(result.current[0]).toBe('on'); 28 | }); 29 | 30 | it('should set specific value when called with argument', () => { 31 | const { result } = renderHook(() => useToggle('on', ['on', 'off'])); 32 | 33 | const toggleFn = result.current[1] as (value: string) => void; 34 | 35 | act(() => { 36 | toggleFn('off'); 37 | }); 38 | 39 | expect(result.current[0]).toBe('off'); 40 | }); 41 | 42 | }); 43 | 44 | describe('useBoolToggle', () => { 45 | it('should initialize with false by default', () => { 46 | const { result } = renderHook(() => useBoolToggle()); 47 | 48 | expect(result.current[0]).toBe(false); 49 | }); 50 | 51 | it('should initialize with provided value', () => { 52 | const { result } = renderHook(() => useBoolToggle(true)); 53 | 54 | expect(result.current[0]).toBe(true); 55 | }); 56 | 57 | it('should toggle between true and false', () => { 58 | const { result } = renderHook(() => useBoolToggle(false)); 59 | 60 | const toggleFn = result.current[1] as () => void; 61 | 62 | act(() => { 63 | toggleFn(); 64 | }); 65 | 66 | expect(result.current[0]).toBe(true); 67 | 68 | act(() => { 69 | toggleFn(); 70 | }); 71 | 72 | expect(result.current[0]).toBe(false); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /lib/hooks/useMutation.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useMutation = ( 4 | apiUrl: string, 5 | payload: any, 6 | headers: Record = {}, 7 | options: { method?: string; timeout?: number } = {}, 8 | onSuccess?: (data: any) => void, 9 | onError?: (error: string) => void 10 | ) => { 11 | const [loading, setLoading] = useState(false); 12 | const [response, setResponse] = useState(null); 13 | const [error, setError] = useState(null); 14 | 15 | useEffect(() => { 16 | let isMounted = true; 17 | const timeoutId = options.timeout ? setTimeout(() => { 18 | if (isMounted) { 19 | setLoading(false); 20 | setError('Request timed out'); 21 | } 22 | }, options.timeout) : undefined; 23 | 24 | setLoading(true); 25 | setError(null); 26 | 27 | fetch(apiUrl, { 28 | method: options.method || 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | ...headers, 32 | }, 33 | body: JSON.stringify(payload), 34 | 35 | }) 36 | .then((response) => { 37 | clearTimeout(timeoutId); 38 | if (!response.ok) { 39 | throw new Error('Request failed'); 40 | } 41 | return response.json(); 42 | }) 43 | .then((data) => { 44 | if (isMounted) { 45 | setLoading(false); 46 | setResponse(data); 47 | if (onSuccess) { 48 | onSuccess(data); 49 | } 50 | } 51 | }) 52 | .catch((error) => { 53 | if (isMounted) { 54 | setLoading(false); 55 | setError(error.message || 'Something went wrong'); 56 | if (onError) { 57 | onError(error.message || 'Something went wrong'); 58 | } 59 | } 60 | }); 61 | 62 | return () => { 63 | isMounted = false; 64 | if (timeoutId) { 65 | clearTimeout(timeoutId); 66 | } 67 | }; 68 | }, [apiUrl, payload, headers, options.timeout, onSuccess, onError]); 69 | 70 | return { loading, response, error }; 71 | }; 72 | 73 | export default useMutation; 74 | -------------------------------------------------------------------------------- /docs/demo/UseCookieListenerDemo.tsx: -------------------------------------------------------------------------------- 1 | import { useCookieListener } from 'react-haiku'; 2 | import React, { useState } from 'react'; 3 | 4 | export const UseCookieListenerDemo = () => { 5 | const [changes, setChanges] = useState([]); 6 | const [cookieName, setCookieName] = useState('demo_cookie'); 7 | const [cookieValue, setCookieValue] = useState(''); 8 | 9 | useCookieListener( 10 | (value, key) => { 11 | setChanges((prev) => [ 12 | `[${new Date().toLocaleTimeString()}] ${key}=${value}`, 13 | ...prev, 14 | ]); 15 | }, 16 | [cookieName], 17 | ); 18 | 19 | return ( 20 |
21 |
22 | setCookieName(e.target.value)} 29 | /> 30 | setCookieValue(e.target.value)} 37 | /> 38 | 47 |
48 | 49 |
50 |

Change Log (last 5):

51 | {changes.slice(0, 5).map((change, i) => ( 52 |
53 | {change} 54 |
55 | ))} 56 |
57 | 58 |

66 | Try changing cookies in another tab or DevTools 67 |

68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/tests/hooks/useNetwork.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook, act } from '@testing-library/react'; 2 | import { useNetwork } from '../../hooks/useNetwork'; 3 | import { useEventListener } from '../../hooks/useEventListener'; 4 | 5 | jest.mock('../../hooks/useEventListener', () => ({ 6 | useEventListener: jest.fn(), 7 | })); 8 | 9 | describe('useNetwork', () => { 10 | const mockNavigator = { onLine: true }; 11 | 12 | beforeAll(() => { 13 | Object.defineProperty(globalThis, 'navigator', { 14 | value: mockNavigator, 15 | writable: true, 16 | }); 17 | }); 18 | 19 | afterEach(() => { 20 | jest.resetAllMocks(); 21 | }); 22 | 23 | it('should return true when navigator.onLine is true', () => { 24 | mockNavigator.onLine = true; 25 | 26 | const { result } = renderHook(() => useNetwork()); 27 | 28 | expect(result.current).toBe(true); 29 | }); 30 | 31 | it('should add event listeners for online and offline', () => { 32 | renderHook(() => useNetwork()); 33 | 34 | expect(useEventListener).toHaveBeenCalledWith('online', expect.any(Function)); 35 | expect(useEventListener).toHaveBeenCalledWith('offline', expect.any(Function)); 36 | }); 37 | 38 | it('should default to true when navigator is undefined', () => { 39 | Object.defineProperty(globalThis, 'navigator', { 40 | value: undefined, 41 | writable: true, 42 | }); 43 | 44 | const { result } = renderHook(() => useNetwork()); 45 | 46 | expect(result.current).toBe(true); 47 | }); 48 | 49 | it ('should return false when handleOffline is called', () => { 50 | const { result } = renderHook(() => useNetwork()); 51 | 52 | const handleOffline = (useEventListener as jest.Mock).mock.calls[1][1]; 53 | 54 | act(() => { 55 | handleOffline(); 56 | }); 57 | 58 | expect(result.current).toBe(false); 59 | }); 60 | it('should return true when handleOnline is called', () => { 61 | const { result } = renderHook(() => useNetwork()); 62 | 63 | const handleOnline = (useEventListener as jest.Mock).mock.calls[0][1]; 64 | 65 | act(() => { 66 | handleOnline(); 67 | }); 68 | 69 | expect(result.current).toBe(true); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /docs/demo/ImageDemo.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'react-haiku'; 2 | import React, { useState } from 'react'; 3 | 4 | export const UseImageDemo = () => { 5 | const [customSrc, setCustomSrc] = useState(''); 6 | const [showBroken, setShowBroken] = useState(false); 7 | 8 | return ( 9 |
10 |
11 | setCustomSrc(e.target.value)} 18 | /> 19 | 25 | 31 |
32 | 33 |
34 |
35 |

Original Image

36 | Demo image 46 |
47 | 48 |
49 |

Fallback Shown

50 | Fallback preview 55 |
56 |
57 | 58 |

66 | Try invalid URLs or click "Load Broken Image" to test fallback 67 |

68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /lib/tests/hooks/useConfirmExit.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useConfirmExit } from '../../hooks/useConfirmExit'; 3 | import { on, off } from '../../helpers/event'; 4 | 5 | jest.mock('../../helpers/event') 6 | 7 | const mockOn = on as jest.MockedFunction; 8 | const mockOff = off as jest.MockedFunction; 9 | 10 | const event = new Event('beforeunload'); 11 | Object.defineProperty(event, 'preventDefault', { value: jest.fn() }); 12 | 13 | describe('useConfirmExit', () => { 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | }); 17 | 18 | it('should add event listener when enabled is true', () => { 19 | renderHook(() => useConfirmExit(true)); 20 | 21 | expect(mockOn).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function)); 22 | }); 23 | 24 | it('should not add event listener when enabled is false', () => { 25 | renderHook(() => useConfirmExit(false)); 26 | 27 | expect(mockOn).not.toHaveBeenCalled(); 28 | }); 29 | 30 | it('should add event listener when enabled function returns true', () => { 31 | const enabledFn = jest.fn().mockReturnValue(true); 32 | renderHook(() => useConfirmExit(enabledFn)); 33 | 34 | expect(on).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function)); 35 | }); 36 | 37 | it('should remove event listener on cleanup', () => { 38 | const { unmount } = renderHook(() => useConfirmExit(true)); 39 | 40 | unmount(); 41 | 42 | expect(mockOff).toHaveBeenCalledWith(window, 'beforeunload', expect.any(Function)); 43 | }); 44 | 45 | it('should call preventDefault', () => { 46 | renderHook(() => useConfirmExit(true, 'Custom message')); 47 | 48 | // Get the handler function. 49 | const handler = mockOn.mock.calls[0]?.[2]; 50 | 51 | handler(event); 52 | 53 | expect(event.preventDefault).toHaveBeenCalled(); 54 | }); 55 | 56 | it('should not call preventDefault when enabled is false', () => { 57 | renderHook(() => useConfirmExit(() => false, 'Custom message')); 58 | 59 | // Get the handler function. 60 | const handler = mockOn.mock.calls[0]?.[2]; 61 | 62 | handler(event); 63 | 64 | expect(event.preventDefault).not.toHaveBeenCalled(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /docs/docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import useBaseUrl from '@docusaurus/useBaseUrl'; 6 | 7 | # Introduction 8 | 9 | 22 | 23 | [![NPM](https://img.shields.io/npm/l/react-haiku)](https://github.com/DavidHDev/react-haiku/blob/main/LICENSE.md) 24 | [![npm](https://img.shields.io/npm/v/react-haiku)](https://www.npmjs.com/package/react-haiku) 25 | [![npm](https://img.shields.io/npm/dm/react-haiku)](https://www.npmjs.com/package/react-haiku) 26 | 27 | ## What is this? 28 | 29 | Haiku is a `simple` & `lightweight` React library with the goal of saving 30 | you time by offering a large collection of `hooks` & `utilities` that will 31 | help you get the job done faster & more efficiently! 32 | 33 |
34 |
35 | 36 | 37 | ## Install 38 | 39 | Installing Haiku is very easy!
40 | *Requires React >=16.8.0* 41 | 42 | #### NPM 43 | ```sh 44 | npm install react-haiku 45 | ``` 46 | 47 | #### Yarn 48 | ```sh 49 | yarn add react-haiku 50 | ``` 51 | #### PNPM 52 | ```sh 53 | pnpm install react-haiku 54 | ``` 55 |
56 | 57 | 58 | ## Using Haiku with NextJS 59 | 60 | Because Haiku uses ES6 modules, it is not transpiled by NextJS automatically since it's an external dependency, so you may have to follow a few extra steps to set it up correctly: 61 | 62 | _NOTE: These steps should no longer be required in Next 14 projects._ 63 | 64 | 1. Add the `next-transpile-modules` package to your project:
65 | 66 | ```sh 67 | npm install next-transpile-modules 68 | ``` 69 | 70 | 2. Configure your project's `next.config.js` file to transpile Haiku: 71 | 72 | ```js 73 | const withTM = require('next-transpile-modules')(['react-haiku']); 74 | module.exports = withTM({}); 75 | ``` 76 | 77 | After following these two steps, importing features from `react-haiku` into your project should work as expected!



78 | 79 |
80 | I hope you will have as much fun using Haiku as I had creating it! 81 |
82 | --------------------------------------------------------------------------------