rm two packages for hooks (#251)

* rm two packages for hooks

* fix navigation
This commit is contained in:
tmshv authored and GitHub committed 2026-02-08 13:31:58 +07:00
1 parent f4c5132d7c
commit dd9058b798
13 files changed
+188 -272

No files matched your search

@@ -1,11 +1,11 @@
import { MenuToggle } from "../MenuToggle"
import { Menu } from "../Menu"
import { Overlay } from "@/ui/Overlay"
import { useContext } from "react"
import { useCallback, useContext } from "react"
import { MenuContext } from "@/context/MenuContext"
import { Box } from "@/ui/Box"
import { Spacer } from "../Spacer"
import { useToggle } from "react-use"
import { useToggle } from "@/hooks/useToggle"
import HudozkaLogo from "../HudozkaLogo"
import { HudozkaTitle } from "../HudozkaTitle"
import Link from "next/link"
@@ -16,6 +16,7 @@ export type MobileNavigationProps = {
export const MobileNavigation: React.FC<MobileNavigationProps> = ({ style }) => {
const [open, toggleOpen] = useToggle(false)
const handleToggle = useCallback(() => toggleOpen(), [toggleOpen])
const menu = useContext(MenuContext)
return (
@@ -32,7 +33,7 @@ export const MobileNavigation: React.FC<MobileNavigationProps> = ({ style }) =>
<Spacer />
<MenuToggle
open={open}
onClick={toggleOpen}
onClick={handleToggle}
/>
</Box>
<Box style={{
@@ -52,7 +53,7 @@ export const MobileNavigation: React.FC<MobileNavigationProps> = ({ style }) =>
<Spacer />
<MenuToggle
open={open}
onClick={toggleOpen}
onClick={handleToggle}
/>
</Box>
</>
+2 -2
View File
@@ -1,5 +1,5 @@
import useMedia from "use-media"
import { useMediaQuery } from "@/hooks/useMediaQuery"
export function useDarkTheme(): boolean {
return useMedia("(prefers-color-scheme: dark)")
return useMediaQuery("(prefers-color-scheme: dark)")
}
+40
View File
@@ -0,0 +1,40 @@
import { renderHook } from "@testing-library/react"
import { describe, it, expect, beforeEach } from "vitest"
import { useLockBodyScroll } from "./useLockBodyScroll"
describe("useLockBodyScroll", () => {
beforeEach(() => {
document.body.style.overflow = ""
})
it("sets overflow hidden when locked", () => {
renderHook(() => useLockBodyScroll(true))
expect(document.body.style.overflow).toBe("hidden")
})
it("does not change overflow when unlocked", () => {
document.body.style.overflow = "auto"
renderHook(() => useLockBodyScroll(false))
expect(document.body.style.overflow).toBe("auto")
})
it("restores original overflow on unmount", () => {
document.body.style.overflow = "scroll"
const { unmount } = renderHook(() => useLockBodyScroll(true))
expect(document.body.style.overflow).toBe("hidden")
unmount()
expect(document.body.style.overflow).toBe("scroll")
})
it("responds to lock/unlock transitions", () => {
const { rerender } = renderHook(
({ locked }) => useLockBodyScroll(locked),
{ initialProps: { locked: false } },
)
expect(document.body.style.overflow).toBe("")
rerender({ locked: true })
expect(document.body.style.overflow).toBe("hidden")
rerender({ locked: false })
expect(document.body.style.overflow).toBe("")
})
})
+16
View File
@@ -0,0 +1,16 @@
import { useEffect } from "react"
export function useLockBodyScroll(locked: boolean): void {
useEffect(() => {
if (!locked) {
return
}
const original = document.body.style.overflow
document.body.style.overflow = "hidden"
return () => {
document.body.style.overflow = original
}
}, [locked])
}
+57
View File
@@ -0,0 +1,57 @@
import { renderHook, act } from "@testing-library/react"
import { describe, it, expect, beforeEach, vi } from "vitest"
import { useMediaQuery } from "./useMediaQuery"
describe("useMediaQuery", () => {
let listeners: Map<string, Set<() => void>>
let matches: Map<string, boolean>
beforeEach(() => {
listeners = new Map()
matches = new Map()
vi.stubGlobal("matchMedia", (query: string) => ({
matches: matches.get(query) ?? false,
addEventListener: (_: string, cb: () => void) => {
if (!listeners.has(query)) {
listeners.set(query, new Set())
}
listeners.get(query)!.add(cb)
},
removeEventListener: (_: string, cb: () => void) => {
listeners.get(query)?.delete(cb)
},
}))
})
it("returns current match state", () => {
matches.set("(min-width: 800px)", true)
const { result } = renderHook(() => useMediaQuery("(min-width: 800px)"))
expect(result.current).toBe(true)
})
it("returns false when query does not match", () => {
const { result } = renderHook(() => useMediaQuery("(min-width: 800px)"))
expect(result.current).toBe(false)
})
it("updates on change events", () => {
const query = "(min-width: 800px)"
const { result } = renderHook(() => useMediaQuery(query))
expect(result.current).toBe(false)
act(() => {
matches.set(query, true)
listeners.get(query)?.forEach((cb) => cb())
})
expect(result.current).toBe(true)
})
it("cleans up listener on unmount", () => {
const query = "(min-width: 800px)"
const { unmount } = renderHook(() => useMediaQuery(query))
expect(listeners.get(query)?.size).toBe(1)
unmount()
expect(listeners.get(query)?.size).toBe(0)
})
})
+13
View File
@@ -0,0 +1,13 @@
import { useSyncExternalStore } from "react"
export function useMediaQuery(query: string, defaultValue = false): boolean {
return useSyncExternalStore(
(callback) => {
const mql = window.matchMedia(query)
mql.addEventListener("change", callback)
return () => mql.removeEventListener("change", callback)
},
() => window.matchMedia(query).matches,
() => defaultValue,
)
}
+2 -2
View File
@@ -1,5 +1,5 @@
import useMedia from "use-media"
import { useMediaQuery } from "@/hooks/useMediaQuery"
export function useMobile(): boolean {
return useMedia("(max-width: 31.25em)", false)
return useMediaQuery("(max-width: 31.25em)", false)
}
+2 -2
View File
@@ -1,5 +1,5 @@
import useMedia from "use-media"
import { useMediaQuery } from "@/hooks/useMediaQuery"
export function useReducedMotion(): boolean {
return useMedia("(prefers-reduced-motion: reduce)")
return useMediaQuery("(prefers-reduced-motion: reduce)")
}
+37
View File
@@ -0,0 +1,37 @@
import { renderHook, act } from "@testing-library/react"
import { describe, it, expect } from "vitest"
import { useToggle } from "./useToggle"
describe("useToggle", () => {
it("returns initial value", () => {
const { result } = renderHook(() => useToggle(false))
expect(result.current[0]).toBe(false)
})
it("toggles value", () => {
const { result } = renderHook(() => useToggle(false))
act(() => result.current[1]())
expect(result.current[0]).toBe(true)
act(() => result.current[1]())
expect(result.current[0]).toBe(false)
})
it("sets explicit true", () => {
const { result } = renderHook(() => useToggle(false))
act(() => result.current[1](true))
expect(result.current[0]).toBe(true)
})
it("sets explicit false", () => {
const { result } = renderHook(() => useToggle(true))
act(() => result.current[1](false))
expect(result.current[0]).toBe(false)
})
it("has a stable dispatch reference", () => {
const { result, rerender } = renderHook(() => useToggle(false))
const first = result.current[1]
rerender()
expect(result.current[1]).toBe(first)
})
})
+8
View File
@@ -0,0 +1,8 @@
import { useReducer } from "react"
export function useToggle(initialValue: boolean): [boolean, (nextValue?: boolean) => void] {
return useReducer(
(state: boolean, next?: boolean) => typeof next === "boolean" ? next : !state,
initialValue,
)
}
+2 -2
View File
@@ -1,8 +1,8 @@
import s from "./styles.module.css"
import { Transition } from "./Transition"
import { useReducedMotion } from "../../hooks/useReducedMotion"
import { useLockBodyScroll } from "react-use"
import { useReducedMotion } from "@/hooks/useReducedMotion"
import { useLockBodyScroll } from "@/hooks/useLockBodyScroll"
export type OverlayProps = {
children?: React.ReactNode