@pyreon/hooks 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/lib/index.d.ts +4 -4
- package/package.json +24 -24
- package/src/__tests__/useBreakpoint.test.ts +48 -48
- package/src/__tests__/useClickOutside.test.ts +31 -31
- package/src/__tests__/useColorScheme.test.ts +22 -22
- package/src/__tests__/useControllableState.test.ts +18 -18
- package/src/__tests__/useDebouncedCallback.test.ts +19 -19
- package/src/__tests__/useDebouncedValue.test.ts +28 -28
- package/src/__tests__/useElementSize.test.ts +21 -21
- package/src/__tests__/useFocus.test.ts +12 -12
- package/src/__tests__/useFocusTrap.test.ts +36 -36
- package/src/__tests__/useHover.test.ts +13 -13
- package/src/__tests__/useIntersection.test.ts +20 -20
- package/src/__tests__/useInterval.test.ts +7 -7
- package/src/__tests__/useIsomorphicLayoutEffect.test.ts +5 -5
- package/src/__tests__/useKeyboard.test.ts +38 -38
- package/src/__tests__/useLatest.test.ts +11 -11
- package/src/__tests__/useMediaQuery.test.ts +29 -29
- package/src/__tests__/useMergedRef.test.ts +10 -10
- package/src/__tests__/usePrevious.test.ts +20 -20
- package/src/__tests__/useReducedMotion.test.ts +15 -15
- package/src/__tests__/useRootSize.test.ts +9 -9
- package/src/__tests__/useScrollLock.test.ts +33 -33
- package/src/__tests__/useSpacing.test.ts +11 -11
- package/src/__tests__/useThemeValue.test.ts +5 -5
- package/src/__tests__/useThrottledCallback.test.ts +16 -16
- package/src/__tests__/useTimeout.test.ts +8 -8
- package/src/__tests__/useToggle.test.ts +14 -14
- package/src/__tests__/useUpdateEffect.test.ts +8 -8
- package/src/__tests__/useWindowResize.test.ts +34 -34
- package/src/index.ts +56 -56
- package/src/useBreakpoint.ts +6 -6
- package/src/useClickOutside.ts +5 -5
- package/src/useClipboard.ts +2 -2
- package/src/useColorScheme.ts +5 -5
- package/src/useControllableState.ts +2 -2
- package/src/useDebouncedCallback.ts +1 -1
- package/src/useDebouncedValue.ts +2 -2
- package/src/useDialog.ts +4 -4
- package/src/useElementSize.ts +2 -2
- package/src/useEventListener.ts +2 -2
- package/src/useFocus.ts +1 -1
- package/src/useFocusTrap.ts +4 -4
- package/src/useHover.ts +1 -1
- package/src/useInfiniteScroll.ts +10 -10
- package/src/useIntersection.ts +2 -2
- package/src/useInterval.ts +1 -1
- package/src/useIsomorphicLayoutEffect.ts +2 -2
- package/src/useKeyboard.ts +3 -3
- package/src/useMediaQuery.ts +4 -4
- package/src/useMergedRef.ts +1 -1
- package/src/useOnline.ts +6 -6
- package/src/usePrevious.ts +1 -1
- package/src/useReducedMotion.ts +2 -2
- package/src/useRootSize.ts +1 -1
- package/src/useScrollLock.ts +3 -3
- package/src/useSpacing.ts +1 -1
- package/src/useThemeValue.ts +2 -2
- package/src/useThrottledCallback.ts +2 -2
- package/src/useTimeAgo.ts +15 -15
- package/src/useTimeout.ts +1 -1
- package/src/useToggle.ts +1 -1
- package/src/useUpdateEffect.ts +2 -2
- package/src/useWindowResize.ts +6 -6
package/README.md
CHANGED
|
@@ -184,12 +184,12 @@ const debouncedSearch = useDebouncedValue(searchTerm, 300)
|
|
|
184
184
|
|
|
185
185
|
## Peer Dependencies
|
|
186
186
|
|
|
187
|
-
| Package
|
|
188
|
-
|
|
|
189
|
-
| @pyreon/core
|
|
187
|
+
| Package | Version |
|
|
188
|
+
| ------------------ | -------- |
|
|
189
|
+
| @pyreon/core | >= 0.0.1 |
|
|
190
190
|
| @pyreon/reactivity | >= 0.0.1 |
|
|
191
|
-
| @pyreon/styler
|
|
192
|
-
| @pyreon/ui-core
|
|
191
|
+
| @pyreon/styler | >= 0.0.1 |
|
|
192
|
+
| @pyreon/ui-core | >= 0.0.1 |
|
|
193
193
|
|
|
194
194
|
## License
|
|
195
195
|
|
package/lib/index.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ declare function useClipboard(options?: {
|
|
|
44
44
|
/**
|
|
45
45
|
* Returns the OS color scheme preference as 'light' or 'dark'.
|
|
46
46
|
*/
|
|
47
|
-
declare function useColorScheme(): () =>
|
|
47
|
+
declare function useColorScheme(): () => 'light' | 'dark';
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/useControllableState.d.ts
|
|
50
50
|
type UseControllableStateOptions<T> = {
|
|
@@ -193,7 +193,7 @@ interface UseInfiniteScrollOptions {
|
|
|
193
193
|
/** Whether there's more data to load. Default: true */
|
|
194
194
|
hasMore?: () => boolean;
|
|
195
195
|
/** Scroll direction. Default: "down" */
|
|
196
|
-
direction?:
|
|
196
|
+
direction?: 'up' | 'down';
|
|
197
197
|
}
|
|
198
198
|
interface UseInfiniteScrollResult {
|
|
199
199
|
/** Attach to the scroll container element. */
|
|
@@ -263,7 +263,7 @@ declare const useIsomorphicLayoutEffect: UseIsomorphicLayoutEffect;
|
|
|
263
263
|
* Listen for a specific key press.
|
|
264
264
|
*/
|
|
265
265
|
declare function useKeyboard(key: string, handler: (event: KeyboardEvent) => void, options?: {
|
|
266
|
-
event?:
|
|
266
|
+
event?: 'keydown' | 'keyup';
|
|
267
267
|
target?: EventTarget;
|
|
268
268
|
}): void;
|
|
269
269
|
//#endregion
|
|
@@ -400,7 +400,7 @@ type UseThrottledCallback = <T extends (...args: any[]) => any>(callback: T, del
|
|
|
400
400
|
declare const useThrottledCallback: UseThrottledCallback;
|
|
401
401
|
//#endregion
|
|
402
402
|
//#region src/useTimeAgo.d.ts
|
|
403
|
-
type TimeUnit =
|
|
403
|
+
type TimeUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
|
|
404
404
|
interface UseTimeAgoOptions {
|
|
405
405
|
/** Custom formatter. Receives the value, unit, and whether it's in the past. */
|
|
406
406
|
formatter?: (value: number, unit: TimeUnit, isPast: boolean) => string;
|
package/package.json
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/hooks",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
|
+
"description": "Signal-based reactive utilities for Pyreon",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/pyreon/pyreon",
|
|
7
9
|
"directory": "packages/fundamentals/hooks"
|
|
8
10
|
},
|
|
9
|
-
"description": "Signal-based reactive utilities for Pyreon",
|
|
10
|
-
"license": "MIT",
|
|
11
|
-
"type": "module",
|
|
12
|
-
"sideEffects": false,
|
|
13
|
-
"exports": {
|
|
14
|
-
".": {
|
|
15
|
-
"bun": "./src/index.ts",
|
|
16
|
-
"import": "./lib/index.js",
|
|
17
|
-
"types": "./lib/index.d.ts"
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"types": "./lib/index.d.ts",
|
|
21
|
-
"main": "./lib/index.js",
|
|
22
11
|
"files": [
|
|
23
12
|
"lib",
|
|
24
13
|
"!lib/**/*.map",
|
|
@@ -27,8 +16,16 @@
|
|
|
27
16
|
"LICENSE",
|
|
28
17
|
"src"
|
|
29
18
|
],
|
|
30
|
-
"
|
|
31
|
-
|
|
19
|
+
"type": "module",
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"main": "./lib/index.js",
|
|
22
|
+
"types": "./lib/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"bun": "./src/index.ts",
|
|
26
|
+
"import": "./lib/index.js",
|
|
27
|
+
"types": "./lib/index.d.ts"
|
|
28
|
+
}
|
|
32
29
|
},
|
|
33
30
|
"publishConfig": {
|
|
34
31
|
"access": "public"
|
|
@@ -37,20 +34,23 @@
|
|
|
37
34
|
"prepublish": "bun run build",
|
|
38
35
|
"build": "bun run vl_rolldown_build",
|
|
39
36
|
"build:watch": "bun run vl_rolldown_build-watch",
|
|
40
|
-
"lint": "
|
|
37
|
+
"lint": "oxlint .",
|
|
41
38
|
"test": "vitest run",
|
|
42
39
|
"test:coverage": "vitest run --coverage",
|
|
43
40
|
"test:watch": "vitest",
|
|
44
41
|
"typecheck": "tsc --noEmit"
|
|
45
42
|
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@pyreon/typescript": "^0.11.6",
|
|
45
|
+
"@vitus-labs/tools-rolldown": "^1.15.3"
|
|
46
|
+
},
|
|
46
47
|
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.11.
|
|
48
|
-
"@pyreon/reactivity": "^0.11.
|
|
49
|
-
"@pyreon/styler": "^0.11.
|
|
50
|
-
"@pyreon/ui-core": "^0.11.
|
|
48
|
+
"@pyreon/core": "^0.11.6",
|
|
49
|
+
"@pyreon/reactivity": "^0.11.6",
|
|
50
|
+
"@pyreon/styler": "^0.11.6",
|
|
51
|
+
"@pyreon/ui-core": "^0.11.6"
|
|
51
52
|
},
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"@pyreon/typescript": "^0.11.5"
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">= 22"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
let mountCallbacks: Array<() => unknown> = []
|
|
4
4
|
let unmountCallbacks: Array<() => void> = []
|
|
5
5
|
|
|
6
|
-
vi.mock(
|
|
6
|
+
vi.mock('@pyreon/core', () => ({
|
|
7
7
|
onMount: (fn: () => unknown) => {
|
|
8
8
|
mountCallbacks.push(fn)
|
|
9
9
|
},
|
|
@@ -12,9 +12,9 @@ vi.mock("@pyreon/core", () => ({
|
|
|
12
12
|
},
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
|
-
import { useBreakpoint } from
|
|
15
|
+
import { useBreakpoint } from '../useBreakpoint'
|
|
16
16
|
|
|
17
|
-
describe(
|
|
17
|
+
describe('useBreakpoint', () => {
|
|
18
18
|
const originalInnerWidth = window.innerWidth
|
|
19
19
|
let rafCallback: FrameRequestCallback | undefined
|
|
20
20
|
|
|
@@ -23,103 +23,103 @@ describe("useBreakpoint", () => {
|
|
|
23
23
|
unmountCallbacks = []
|
|
24
24
|
rafCallback = undefined
|
|
25
25
|
|
|
26
|
-
vi.spyOn(window,
|
|
26
|
+
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
|
27
27
|
rafCallback = cb
|
|
28
28
|
return 1
|
|
29
29
|
})
|
|
30
|
-
vi.spyOn(window,
|
|
30
|
+
vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(() => {
|
|
31
31
|
/* no-op */
|
|
32
32
|
})
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
afterEach(() => {
|
|
36
|
-
Object.defineProperty(window,
|
|
36
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: originalInnerWidth })
|
|
37
37
|
vi.restoreAllMocks()
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it(
|
|
41
|
-
Object.defineProperty(window,
|
|
40
|
+
it('returns the active breakpoint based on window width', () => {
|
|
41
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 800 })
|
|
42
42
|
const bp = useBreakpoint()
|
|
43
|
-
expect(bp()).toBe(
|
|
43
|
+
expect(bp()).toBe('md') // 768 <= 800 < 992
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
it(
|
|
47
|
-
Object.defineProperty(window,
|
|
46
|
+
it('uses default breakpoints', () => {
|
|
47
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 400 })
|
|
48
48
|
const bp = useBreakpoint()
|
|
49
|
-
expect(bp()).toBe(
|
|
49
|
+
expect(bp()).toBe('xs') // 0 <= 400 < 576
|
|
50
50
|
|
|
51
|
-
Object.defineProperty(window,
|
|
51
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 600 })
|
|
52
52
|
const bp2 = useBreakpoint()
|
|
53
|
-
expect(bp2()).toBe(
|
|
53
|
+
expect(bp2()).toBe('sm') // 576 <= 600 < 768
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
it(
|
|
57
|
-
Object.defineProperty(window,
|
|
56
|
+
it('supports custom breakpoints', () => {
|
|
57
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 500 })
|
|
58
58
|
const bp = useBreakpoint({ mobile: 0, tablet: 600, desktop: 1024 })
|
|
59
|
-
expect(bp()).toBe(
|
|
59
|
+
expect(bp()).toBe('mobile')
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
it(
|
|
63
|
-
Object.defineProperty(window,
|
|
62
|
+
it('returns largest matching breakpoint', () => {
|
|
63
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 1200 })
|
|
64
64
|
const bp = useBreakpoint()
|
|
65
|
-
expect(bp()).toBe(
|
|
65
|
+
expect(bp()).toBe('xl')
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
it(
|
|
69
|
-
Object.defineProperty(window,
|
|
68
|
+
it('returns first breakpoint for very small widths', () => {
|
|
69
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 0 })
|
|
70
70
|
const bp = useBreakpoint()
|
|
71
|
-
expect(bp()).toBe(
|
|
71
|
+
expect(bp()).toBe('xs')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
it(
|
|
75
|
-
Object.defineProperty(window,
|
|
74
|
+
it('updates breakpoint on resize', () => {
|
|
75
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 800 })
|
|
76
76
|
const bp = useBreakpoint()
|
|
77
77
|
mountCallbacks.forEach((cb) => {
|
|
78
78
|
cb()
|
|
79
79
|
})
|
|
80
|
-
expect(bp()).toBe(
|
|
80
|
+
expect(bp()).toBe('md')
|
|
81
81
|
|
|
82
82
|
// Simulate resize
|
|
83
|
-
Object.defineProperty(window,
|
|
84
|
-
window.dispatchEvent(new Event(
|
|
83
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 1200 })
|
|
84
|
+
window.dispatchEvent(new Event('resize'))
|
|
85
85
|
|
|
86
86
|
// Execute rAF callback
|
|
87
87
|
rafCallback?.(0)
|
|
88
|
-
expect(bp()).toBe(
|
|
88
|
+
expect(bp()).toBe('xl')
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
it(
|
|
92
|
-
Object.defineProperty(window,
|
|
91
|
+
it('debounces resize with requestAnimationFrame', () => {
|
|
92
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 800 })
|
|
93
93
|
const bp = useBreakpoint()
|
|
94
94
|
mountCallbacks.forEach((cb) => {
|
|
95
95
|
cb()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
Object.defineProperty(window,
|
|
99
|
-
window.dispatchEvent(new Event(
|
|
98
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 1200 })
|
|
99
|
+
window.dispatchEvent(new Event('resize'))
|
|
100
100
|
|
|
101
101
|
// Before rAF fires, value should still be old
|
|
102
|
-
expect(bp()).toBe(
|
|
102
|
+
expect(bp()).toBe('md')
|
|
103
103
|
|
|
104
104
|
rafCallback?.(0)
|
|
105
|
-
expect(bp()).toBe(
|
|
105
|
+
expect(bp()).toBe('xl')
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
it(
|
|
109
|
-
Object.defineProperty(window,
|
|
108
|
+
it('cancels pending rAF on new resize', () => {
|
|
109
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 800 })
|
|
110
110
|
useBreakpoint()
|
|
111
111
|
mountCallbacks.forEach((cb) => {
|
|
112
112
|
cb()
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
window.dispatchEvent(new Event(
|
|
116
|
-
window.dispatchEvent(new Event(
|
|
115
|
+
window.dispatchEvent(new Event('resize'))
|
|
116
|
+
window.dispatchEvent(new Event('resize'))
|
|
117
117
|
|
|
118
118
|
expect(window.cancelAnimationFrame).toHaveBeenCalled()
|
|
119
119
|
})
|
|
120
120
|
|
|
121
|
-
it(
|
|
122
|
-
const removeSpy = vi.spyOn(window,
|
|
121
|
+
it('cleans up on unmount', () => {
|
|
122
|
+
const removeSpy = vi.spyOn(window, 'removeEventListener')
|
|
123
123
|
useBreakpoint()
|
|
124
124
|
mountCallbacks.forEach((cb) => {
|
|
125
125
|
cb()
|
|
@@ -128,21 +128,21 @@ describe("useBreakpoint", () => {
|
|
|
128
128
|
cb()
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
expect(removeSpy).toHaveBeenCalledWith(
|
|
131
|
+
expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function))
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
it(
|
|
135
|
-
Object.defineProperty(window,
|
|
134
|
+
it('does not update when breakpoint has not changed', () => {
|
|
135
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 800 })
|
|
136
136
|
const bp = useBreakpoint()
|
|
137
137
|
mountCallbacks.forEach((cb) => {
|
|
138
138
|
cb()
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
// Resize but stay within same breakpoint
|
|
142
|
-
Object.defineProperty(window,
|
|
143
|
-
window.dispatchEvent(new Event(
|
|
142
|
+
Object.defineProperty(window, 'innerWidth', { writable: true, value: 850 })
|
|
143
|
+
window.dispatchEvent(new Event('resize'))
|
|
144
144
|
rafCallback?.(0)
|
|
145
145
|
|
|
146
|
-
expect(bp()).toBe(
|
|
146
|
+
expect(bp()).toBe('md') // Still md (768-991)
|
|
147
147
|
})
|
|
148
148
|
})
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
// Capture registered callbacks so we can invoke lifecycle manually
|
|
4
4
|
let mountCallbacks: Array<() => void> = []
|
|
5
5
|
let unmountCallbacks: Array<() => void> = []
|
|
6
6
|
|
|
7
|
-
vi.mock(
|
|
7
|
+
vi.mock('@pyreon/core', () => ({
|
|
8
8
|
onMount: (fn: () => unknown) => {
|
|
9
9
|
mountCallbacks.push(fn as () => void)
|
|
10
10
|
},
|
|
@@ -13,15 +13,15 @@ vi.mock("@pyreon/core", () => ({
|
|
|
13
13
|
},
|
|
14
14
|
}))
|
|
15
15
|
|
|
16
|
-
import { useClickOutside } from
|
|
16
|
+
import { useClickOutside } from '../useClickOutside'
|
|
17
17
|
|
|
18
|
-
describe(
|
|
18
|
+
describe('useClickOutside', () => {
|
|
19
19
|
let container: HTMLDivElement
|
|
20
20
|
|
|
21
21
|
beforeEach(() => {
|
|
22
22
|
mountCallbacks = []
|
|
23
23
|
unmountCallbacks = []
|
|
24
|
-
container = document.createElement(
|
|
24
|
+
container = document.createElement('div')
|
|
25
25
|
document.body.appendChild(container)
|
|
26
26
|
})
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ describe("useClickOutside", () => {
|
|
|
29
29
|
document.body.removeChild(container)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
it(
|
|
32
|
+
it('calls handler when clicking outside the element', () => {
|
|
33
33
|
const handler = vi.fn()
|
|
34
34
|
useClickOutside(() => container, handler)
|
|
35
35
|
|
|
@@ -38,20 +38,20 @@ describe("useClickOutside", () => {
|
|
|
38
38
|
cb()
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
const outside = document.createElement(
|
|
41
|
+
const outside = document.createElement('div')
|
|
42
42
|
document.body.appendChild(outside)
|
|
43
43
|
|
|
44
|
-
const event = new MouseEvent(
|
|
45
|
-
Object.defineProperty(event,
|
|
44
|
+
const event = new MouseEvent('mousedown', { bubbles: true })
|
|
45
|
+
Object.defineProperty(event, 'target', { value: outside })
|
|
46
46
|
document.dispatchEvent(event)
|
|
47
47
|
|
|
48
48
|
expect(handler).toHaveBeenCalledTimes(1)
|
|
49
49
|
document.body.removeChild(outside)
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
it(
|
|
52
|
+
it('does not call handler when clicking inside the element', () => {
|
|
53
53
|
const handler = vi.fn()
|
|
54
|
-
const child = document.createElement(
|
|
54
|
+
const child = document.createElement('span')
|
|
55
55
|
container.appendChild(child)
|
|
56
56
|
|
|
57
57
|
useClickOutside(() => container, handler)
|
|
@@ -59,86 +59,86 @@ describe("useClickOutside", () => {
|
|
|
59
59
|
cb()
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
const event = new MouseEvent(
|
|
63
|
-
Object.defineProperty(event,
|
|
62
|
+
const event = new MouseEvent('mousedown', { bubbles: true })
|
|
63
|
+
Object.defineProperty(event, 'target', { value: child })
|
|
64
64
|
document.dispatchEvent(event)
|
|
65
65
|
|
|
66
66
|
expect(handler).not.toHaveBeenCalled()
|
|
67
67
|
})
|
|
68
68
|
|
|
69
|
-
it(
|
|
69
|
+
it('does not call handler when clicking the element itself', () => {
|
|
70
70
|
const handler = vi.fn()
|
|
71
71
|
useClickOutside(() => container, handler)
|
|
72
72
|
mountCallbacks.forEach((cb) => {
|
|
73
73
|
cb()
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
const event = new MouseEvent(
|
|
77
|
-
Object.defineProperty(event,
|
|
76
|
+
const event = new MouseEvent('mousedown', { bubbles: true })
|
|
77
|
+
Object.defineProperty(event, 'target', { value: container })
|
|
78
78
|
document.dispatchEvent(event)
|
|
79
79
|
|
|
80
80
|
expect(handler).not.toHaveBeenCalled()
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
it(
|
|
83
|
+
it('does not call handler when element is null', () => {
|
|
84
84
|
const handler = vi.fn()
|
|
85
85
|
useClickOutside(() => null, handler)
|
|
86
86
|
mountCallbacks.forEach((cb) => {
|
|
87
87
|
cb()
|
|
88
88
|
})
|
|
89
89
|
|
|
90
|
-
const event = new MouseEvent(
|
|
91
|
-
Object.defineProperty(event,
|
|
90
|
+
const event = new MouseEvent('mousedown', { bubbles: true })
|
|
91
|
+
Object.defineProperty(event, 'target', { value: document.body })
|
|
92
92
|
document.dispatchEvent(event)
|
|
93
93
|
|
|
94
94
|
expect(handler).not.toHaveBeenCalled()
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
-
it(
|
|
97
|
+
it('handles touchstart events', () => {
|
|
98
98
|
const handler = vi.fn()
|
|
99
99
|
useClickOutside(() => container, handler)
|
|
100
100
|
mountCallbacks.forEach((cb) => {
|
|
101
101
|
cb()
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
const outside = document.createElement(
|
|
104
|
+
const outside = document.createElement('div')
|
|
105
105
|
document.body.appendChild(outside)
|
|
106
106
|
|
|
107
|
-
const event = new Event(
|
|
108
|
-
Object.defineProperty(event,
|
|
107
|
+
const event = new Event('touchstart', { bubbles: true })
|
|
108
|
+
Object.defineProperty(event, 'target', { value: outside })
|
|
109
109
|
document.dispatchEvent(event)
|
|
110
110
|
|
|
111
111
|
expect(handler).toHaveBeenCalledTimes(1)
|
|
112
112
|
document.body.removeChild(outside)
|
|
113
113
|
})
|
|
114
114
|
|
|
115
|
-
it(
|
|
115
|
+
it('removes listeners on unmount', () => {
|
|
116
116
|
const handler = vi.fn()
|
|
117
117
|
useClickOutside(() => container, handler)
|
|
118
118
|
mountCallbacks.forEach((cb) => {
|
|
119
119
|
cb()
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
-
const removeSpy = vi.spyOn(document,
|
|
122
|
+
const removeSpy = vi.spyOn(document, 'removeEventListener')
|
|
123
123
|
unmountCallbacks.forEach((cb) => {
|
|
124
124
|
cb()
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
expect(removeSpy).toHaveBeenCalledWith(
|
|
128
|
-
expect(removeSpy).toHaveBeenCalledWith(
|
|
127
|
+
expect(removeSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true)
|
|
128
|
+
expect(removeSpy).toHaveBeenCalledWith('touchstart', expect.any(Function), true)
|
|
129
129
|
removeSpy.mockRestore()
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
it(
|
|
132
|
+
it('adds listeners with capture phase', () => {
|
|
133
133
|
const handler = vi.fn()
|
|
134
|
-
const addSpy = vi.spyOn(document,
|
|
134
|
+
const addSpy = vi.spyOn(document, 'addEventListener')
|
|
135
135
|
useClickOutside(() => container, handler)
|
|
136
136
|
mountCallbacks.forEach((cb) => {
|
|
137
137
|
cb()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
expect(addSpy).toHaveBeenCalledWith(
|
|
141
|
-
expect(addSpy).toHaveBeenCalledWith(
|
|
140
|
+
expect(addSpy).toHaveBeenCalledWith('mousedown', expect.any(Function), true)
|
|
141
|
+
expect(addSpy).toHaveBeenCalledWith('touchstart', expect.any(Function), true)
|
|
142
142
|
addSpy.mockRestore()
|
|
143
143
|
})
|
|
144
144
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
let mountCallbacks: Array<() => unknown> = []
|
|
4
4
|
let unmountCallbacks: Array<() => void> = []
|
|
5
5
|
|
|
6
|
-
vi.mock(
|
|
6
|
+
vi.mock('@pyreon/core', () => ({
|
|
7
7
|
onMount: (fn: () => unknown) => {
|
|
8
8
|
mountCallbacks.push(fn)
|
|
9
9
|
},
|
|
@@ -12,9 +12,9 @@ vi.mock("@pyreon/core", () => ({
|
|
|
12
12
|
},
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
|
-
import { useColorScheme } from
|
|
15
|
+
import { useColorScheme } from '../useColorScheme'
|
|
16
16
|
|
|
17
|
-
describe(
|
|
17
|
+
describe('useColorScheme', () => {
|
|
18
18
|
let changeListeners: Map<string, (e: MediaQueryListEvent) => void>
|
|
19
19
|
|
|
20
20
|
beforeEach(() => {
|
|
@@ -22,35 +22,35 @@ describe("useColorScheme", () => {
|
|
|
22
22
|
unmountCallbacks = []
|
|
23
23
|
changeListeners = new Map()
|
|
24
24
|
|
|
25
|
-
Object.defineProperty(window,
|
|
25
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
26
26
|
writable: true,
|
|
27
27
|
value: vi.fn((query: string) => ({
|
|
28
28
|
matches: false,
|
|
29
29
|
media: query,
|
|
30
30
|
addEventListener: vi.fn((event: string, cb: (e: MediaQueryListEvent) => void) => {
|
|
31
|
-
if (event ===
|
|
31
|
+
if (event === 'change') changeListeners.set(query, cb)
|
|
32
32
|
}),
|
|
33
33
|
removeEventListener: vi.fn(),
|
|
34
34
|
})),
|
|
35
35
|
})
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
38
|
+
it('returns light by default', () => {
|
|
39
39
|
const scheme = useColorScheme()
|
|
40
40
|
mountCallbacks.forEach((cb) => {
|
|
41
41
|
cb()
|
|
42
42
|
})
|
|
43
|
-
expect(scheme()).toBe(
|
|
43
|
+
expect(scheme()).toBe('light')
|
|
44
44
|
})
|
|
45
45
|
|
|
46
|
-
it(
|
|
47
|
-
Object.defineProperty(window,
|
|
46
|
+
it('returns dark when prefers-color-scheme is dark', () => {
|
|
47
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
48
48
|
writable: true,
|
|
49
49
|
value: vi.fn((query: string) => ({
|
|
50
50
|
matches: true,
|
|
51
51
|
media: query,
|
|
52
52
|
addEventListener: vi.fn((event: string, cb: (e: MediaQueryListEvent) => void) => {
|
|
53
|
-
if (event ===
|
|
53
|
+
if (event === 'change') changeListeners.set(query, cb)
|
|
54
54
|
}),
|
|
55
55
|
removeEventListener: vi.fn(),
|
|
56
56
|
})),
|
|
@@ -60,29 +60,29 @@ describe("useColorScheme", () => {
|
|
|
60
60
|
mountCallbacks.forEach((cb) => {
|
|
61
61
|
cb()
|
|
62
62
|
})
|
|
63
|
-
expect(scheme()).toBe(
|
|
63
|
+
expect(scheme()).toBe('dark')
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
it(
|
|
66
|
+
it('updates when color scheme changes from light to dark', () => {
|
|
67
67
|
const scheme = useColorScheme()
|
|
68
68
|
mountCallbacks.forEach((cb) => {
|
|
69
69
|
cb()
|
|
70
70
|
})
|
|
71
|
-
expect(scheme()).toBe(
|
|
71
|
+
expect(scheme()).toBe('light')
|
|
72
72
|
|
|
73
|
-
const listener = changeListeners.get(
|
|
73
|
+
const listener = changeListeners.get('(prefers-color-scheme: dark)')
|
|
74
74
|
listener?.({ matches: true } as MediaQueryListEvent)
|
|
75
|
-
expect(scheme()).toBe(
|
|
75
|
+
expect(scheme()).toBe('dark')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it(
|
|
79
|
-
Object.defineProperty(window,
|
|
78
|
+
it('updates when color scheme changes from dark to light', () => {
|
|
79
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
80
80
|
writable: true,
|
|
81
81
|
value: vi.fn((query: string) => ({
|
|
82
82
|
matches: true,
|
|
83
83
|
media: query,
|
|
84
84
|
addEventListener: vi.fn((event: string, cb: (e: MediaQueryListEvent) => void) => {
|
|
85
|
-
if (event ===
|
|
85
|
+
if (event === 'change') changeListeners.set(query, cb)
|
|
86
86
|
}),
|
|
87
87
|
removeEventListener: vi.fn(),
|
|
88
88
|
})),
|
|
@@ -92,10 +92,10 @@ describe("useColorScheme", () => {
|
|
|
92
92
|
mountCallbacks.forEach((cb) => {
|
|
93
93
|
cb()
|
|
94
94
|
})
|
|
95
|
-
expect(scheme()).toBe(
|
|
95
|
+
expect(scheme()).toBe('dark')
|
|
96
96
|
|
|
97
|
-
const listener = changeListeners.get(
|
|
97
|
+
const listener = changeListeners.get('(prefers-color-scheme: dark)')
|
|
98
98
|
listener?.({ matches: false } as MediaQueryListEvent)
|
|
99
|
-
expect(scheme()).toBe(
|
|
99
|
+
expect(scheme()).toBe('light')
|
|
100
100
|
})
|
|
101
101
|
})
|