@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
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from
|
|
2
|
-
import { useControllableState } from
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useControllableState } from '../useControllableState'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
6
|
-
const [value] = useControllableState({ defaultValue:
|
|
7
|
-
expect(value()).toBe(
|
|
4
|
+
describe('useControllableState', () => {
|
|
5
|
+
it('uses defaultValue when uncontrolled', () => {
|
|
6
|
+
const [value] = useControllableState({ defaultValue: 'hello' })
|
|
7
|
+
expect(value()).toBe('hello')
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it(
|
|
10
|
+
it('updates internal state when uncontrolled', () => {
|
|
11
11
|
const [value, setValue] = useControllableState({ defaultValue: 0 })
|
|
12
12
|
setValue(5)
|
|
13
13
|
expect(value()).toBe(5)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
it(
|
|
17
|
-
const [value] = useControllableState({ value:
|
|
18
|
-
expect(value()).toBe(
|
|
16
|
+
it('uses value when controlled', () => {
|
|
17
|
+
const [value] = useControllableState({ value: 'controlled', defaultValue: 'default' })
|
|
18
|
+
expect(value()).toBe('controlled')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
it(
|
|
21
|
+
it('does not update internal state when controlled', () => {
|
|
22
22
|
const onChange = vi.fn()
|
|
23
23
|
const [value, setValue] = useControllableState({
|
|
24
|
-
value:
|
|
25
|
-
defaultValue:
|
|
24
|
+
value: 'controlled',
|
|
25
|
+
defaultValue: 'default',
|
|
26
26
|
onChange,
|
|
27
27
|
})
|
|
28
|
-
setValue(
|
|
29
|
-
expect(value()).toBe(
|
|
30
|
-
expect(onChange).toHaveBeenCalledWith(
|
|
28
|
+
setValue('new')
|
|
29
|
+
expect(value()).toBe('controlled')
|
|
30
|
+
expect(onChange).toHaveBeenCalledWith('new')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
it(
|
|
33
|
+
it('calls onChange in uncontrolled mode', () => {
|
|
34
34
|
const onChange = vi.fn()
|
|
35
35
|
const [, setValue] = useControllableState({ defaultValue: 0, onChange })
|
|
36
36
|
setValue(10)
|
|
37
37
|
expect(onChange).toHaveBeenCalledWith(10)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it(
|
|
40
|
+
it('supports updater function', () => {
|
|
41
41
|
const [value, setValue] = useControllableState({ defaultValue: 1 })
|
|
42
42
|
setValue((prev: number) => prev + 1)
|
|
43
43
|
expect(value()).toBe(2)
|
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
2
|
-
import { useDebouncedCallback } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useDebouncedCallback } from '../useDebouncedCallback'
|
|
3
3
|
|
|
4
4
|
// Mock onUnmount since it requires component lifecycle context
|
|
5
|
-
vi.mock(
|
|
5
|
+
vi.mock('@pyreon/core', () => ({
|
|
6
6
|
onMount: (fn: () => void) => fn(),
|
|
7
7
|
onUnmount: (_fn: () => void) => {
|
|
8
8
|
/* no-op */
|
|
9
9
|
},
|
|
10
10
|
}))
|
|
11
11
|
|
|
12
|
-
describe(
|
|
12
|
+
describe('useDebouncedCallback', () => {
|
|
13
13
|
beforeEach(() => vi.useFakeTimers())
|
|
14
14
|
afterEach(() => vi.useRealTimers())
|
|
15
15
|
|
|
16
|
-
it(
|
|
16
|
+
it('debounces the callback', () => {
|
|
17
17
|
const fn = vi.fn()
|
|
18
18
|
const debounced = useDebouncedCallback(fn, 100)
|
|
19
19
|
|
|
20
|
-
debounced(
|
|
21
|
-
debounced(
|
|
22
|
-
debounced(
|
|
20
|
+
debounced('a')
|
|
21
|
+
debounced('b')
|
|
22
|
+
debounced('c')
|
|
23
23
|
|
|
24
24
|
expect(fn).not.toHaveBeenCalled()
|
|
25
25
|
vi.advanceTimersByTime(100)
|
|
26
26
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
27
|
-
expect(fn).toHaveBeenCalledWith(
|
|
27
|
+
expect(fn).toHaveBeenCalledWith('c')
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
it(
|
|
30
|
+
it('cancel prevents the callback', () => {
|
|
31
31
|
const fn = vi.fn()
|
|
32
32
|
const debounced = useDebouncedCallback(fn, 100)
|
|
33
33
|
|
|
34
|
-
debounced(
|
|
34
|
+
debounced('a')
|
|
35
35
|
debounced.cancel()
|
|
36
36
|
vi.advanceTimersByTime(200)
|
|
37
37
|
|
|
38
38
|
expect(fn).not.toHaveBeenCalled()
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it(
|
|
41
|
+
it('flush invokes immediately', () => {
|
|
42
42
|
const fn = vi.fn()
|
|
43
43
|
const debounced = useDebouncedCallback(fn, 100)
|
|
44
44
|
|
|
45
|
-
debounced(
|
|
45
|
+
debounced('x')
|
|
46
46
|
debounced.flush()
|
|
47
47
|
|
|
48
|
-
expect(fn).toHaveBeenCalledWith(
|
|
48
|
+
expect(fn).toHaveBeenCalledWith('x')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it(
|
|
51
|
+
it('flush is a no-op when no pending timer', () => {
|
|
52
52
|
const fn = vi.fn()
|
|
53
53
|
const debounced = useDebouncedCallback(fn, 100)
|
|
54
54
|
|
|
@@ -56,11 +56,11 @@ describe("useDebouncedCallback", () => {
|
|
|
56
56
|
expect(fn).not.toHaveBeenCalled()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
it(
|
|
59
|
+
it('flush is a no-op after timer already fired', () => {
|
|
60
60
|
const fn = vi.fn()
|
|
61
61
|
const debounced = useDebouncedCallback(fn, 100)
|
|
62
62
|
|
|
63
|
-
debounced(
|
|
63
|
+
debounced('a')
|
|
64
64
|
vi.advanceTimersByTime(100)
|
|
65
65
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
66
66
|
|
|
@@ -68,11 +68,11 @@ describe("useDebouncedCallback", () => {
|
|
|
68
68
|
expect(fn).toHaveBeenCalledTimes(1)
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
it(
|
|
71
|
+
it('flush is a no-op after cancel', () => {
|
|
72
72
|
const fn = vi.fn()
|
|
73
73
|
const debounced = useDebouncedCallback(fn, 100)
|
|
74
74
|
|
|
75
|
-
debounced(
|
|
75
|
+
debounced('a')
|
|
76
76
|
debounced.cancel()
|
|
77
77
|
debounced.flush()
|
|
78
78
|
expect(fn).not.toHaveBeenCalled()
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { signal } from
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from
|
|
3
|
-
import { useDebouncedValue } from
|
|
1
|
+
import { signal } from '@pyreon/reactivity'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { useDebouncedValue } from '../useDebouncedValue'
|
|
4
4
|
|
|
5
5
|
// Mock onUnmount since it requires component lifecycle context
|
|
6
|
-
vi.mock(
|
|
6
|
+
vi.mock('@pyreon/core', () => ({
|
|
7
7
|
onMount: (fn: () => void) => fn(),
|
|
8
8
|
onUnmount: (_fn: () => void) => {
|
|
9
9
|
/* no-op */
|
|
10
10
|
},
|
|
11
11
|
}))
|
|
12
12
|
|
|
13
|
-
describe(
|
|
13
|
+
describe('useDebouncedValue', () => {
|
|
14
14
|
beforeEach(() => {
|
|
15
15
|
vi.useFakeTimers()
|
|
16
16
|
})
|
|
@@ -19,33 +19,33 @@ describe("useDebouncedValue", () => {
|
|
|
19
19
|
vi.useRealTimers()
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it(
|
|
22
|
+
it('returns the initial value immediately', () => {
|
|
23
23
|
const source = signal(42)
|
|
24
24
|
const debounced = useDebouncedValue(source, 300)
|
|
25
25
|
expect(debounced()).toBe(42)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
it(
|
|
29
|
-
const source = signal(
|
|
28
|
+
it('does not update immediately when source changes', () => {
|
|
29
|
+
const source = signal('hello')
|
|
30
30
|
const debounced = useDebouncedValue(source, 300)
|
|
31
|
-
expect(debounced()).toBe(
|
|
31
|
+
expect(debounced()).toBe('hello')
|
|
32
32
|
|
|
33
|
-
source.set(
|
|
34
|
-
expect(debounced()).toBe(
|
|
33
|
+
source.set('world')
|
|
34
|
+
expect(debounced()).toBe('hello')
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
it(
|
|
38
|
-
const source = signal(
|
|
37
|
+
it('updates after the delay elapses', () => {
|
|
38
|
+
const source = signal('hello')
|
|
39
39
|
const debounced = useDebouncedValue(source, 300)
|
|
40
40
|
|
|
41
|
-
source.set(
|
|
42
|
-
expect(debounced()).toBe(
|
|
41
|
+
source.set('world')
|
|
42
|
+
expect(debounced()).toBe('hello')
|
|
43
43
|
|
|
44
44
|
vi.advanceTimersByTime(300)
|
|
45
|
-
expect(debounced()).toBe(
|
|
45
|
+
expect(debounced()).toBe('world')
|
|
46
46
|
})
|
|
47
47
|
|
|
48
|
-
it(
|
|
48
|
+
it('resets the timer on rapid changes', () => {
|
|
49
49
|
const source = signal(1)
|
|
50
50
|
const debounced = useDebouncedValue(source, 300)
|
|
51
51
|
|
|
@@ -62,7 +62,7 @@ describe("useDebouncedValue", () => {
|
|
|
62
62
|
expect(debounced()).toBe(4)
|
|
63
63
|
})
|
|
64
64
|
|
|
65
|
-
it(
|
|
65
|
+
it('only applies the last value after debounce', () => {
|
|
66
66
|
const source = signal(0)
|
|
67
67
|
const debounced = useDebouncedValue(source, 200)
|
|
68
68
|
|
|
@@ -76,20 +76,20 @@ describe("useDebouncedValue", () => {
|
|
|
76
76
|
expect(debounced()).toBe(5)
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
it(
|
|
80
|
-
const source = signal(
|
|
79
|
+
it('handles multiple debounce cycles', () => {
|
|
80
|
+
const source = signal('a')
|
|
81
81
|
const debounced = useDebouncedValue(source, 100)
|
|
82
82
|
|
|
83
|
-
source.set(
|
|
83
|
+
source.set('b')
|
|
84
84
|
vi.advanceTimersByTime(100)
|
|
85
|
-
expect(debounced()).toBe(
|
|
85
|
+
expect(debounced()).toBe('b')
|
|
86
86
|
|
|
87
|
-
source.set(
|
|
87
|
+
source.set('c')
|
|
88
88
|
vi.advanceTimersByTime(100)
|
|
89
|
-
expect(debounced()).toBe(
|
|
89
|
+
expect(debounced()).toBe('c')
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
it(
|
|
92
|
+
it('works with zero delay', () => {
|
|
93
93
|
const source = signal(1)
|
|
94
94
|
const debounced = useDebouncedValue(source, 0)
|
|
95
95
|
|
|
@@ -98,9 +98,9 @@ describe("useDebouncedValue", () => {
|
|
|
98
98
|
expect(debounced()).toBe(2)
|
|
99
99
|
})
|
|
100
100
|
|
|
101
|
-
it(
|
|
102
|
-
const obj1 = { name:
|
|
103
|
-
const obj2 = { name:
|
|
101
|
+
it('works with object values', () => {
|
|
102
|
+
const obj1 = { name: 'Alice' }
|
|
103
|
+
const obj2 = { name: 'Bob' }
|
|
104
104
|
const source = signal(obj1)
|
|
105
105
|
const debounced = useDebouncedValue(source, 100)
|
|
106
106
|
expect(debounced()).toBe(obj1)
|
|
@@ -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 { useElementSize } from
|
|
15
|
+
import { useElementSize } from '../useElementSize'
|
|
16
16
|
|
|
17
|
-
describe(
|
|
17
|
+
describe('useElementSize', () => {
|
|
18
18
|
let resizeCallback: ((entries: ResizeObserverEntry[]) => void) | undefined
|
|
19
19
|
let observeSpy: ReturnType<typeof vi.fn>
|
|
20
20
|
let disconnectSpy: ReturnType<typeof vi.fn>
|
|
@@ -36,15 +36,15 @@ describe("useElementSize", () => {
|
|
|
36
36
|
}) as unknown as typeof ResizeObserver
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
it(
|
|
39
|
+
it('returns initial size of 0x0', () => {
|
|
40
40
|
const size = useElementSize(() => null)
|
|
41
41
|
expect(size().width).toBe(0)
|
|
42
42
|
expect(size().height).toBe(0)
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
it(
|
|
46
|
-
const el = document.createElement(
|
|
47
|
-
vi.spyOn(el,
|
|
45
|
+
it('measures initial element size on mount', () => {
|
|
46
|
+
const el = document.createElement('div')
|
|
47
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
48
48
|
width: 200,
|
|
49
49
|
height: 100,
|
|
50
50
|
x: 0,
|
|
@@ -67,9 +67,9 @@ describe("useElementSize", () => {
|
|
|
67
67
|
expect(size().height).toBe(100)
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
-
it(
|
|
71
|
-
const el = document.createElement(
|
|
72
|
-
vi.spyOn(el,
|
|
70
|
+
it('observes the element with ResizeObserver', () => {
|
|
71
|
+
const el = document.createElement('div')
|
|
72
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
73
73
|
width: 0,
|
|
74
74
|
height: 0,
|
|
75
75
|
x: 0,
|
|
@@ -91,9 +91,9 @@ describe("useElementSize", () => {
|
|
|
91
91
|
expect(observeSpy).toHaveBeenCalledWith(el)
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
it(
|
|
95
|
-
const el = document.createElement(
|
|
96
|
-
vi.spyOn(el,
|
|
94
|
+
it('updates size when ResizeObserver fires', () => {
|
|
95
|
+
const el = document.createElement('div')
|
|
96
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
97
97
|
width: 100,
|
|
98
98
|
height: 50,
|
|
99
99
|
x: 0,
|
|
@@ -124,7 +124,7 @@ describe("useElementSize", () => {
|
|
|
124
124
|
expect(size().height).toBe(150)
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
-
it(
|
|
127
|
+
it('does nothing on mount when element is null', () => {
|
|
128
128
|
useElementSize(() => null)
|
|
129
129
|
mountCallbacks.forEach((cb) => {
|
|
130
130
|
cb()
|
|
@@ -133,9 +133,9 @@ describe("useElementSize", () => {
|
|
|
133
133
|
expect(observeSpy).not.toHaveBeenCalled()
|
|
134
134
|
})
|
|
135
135
|
|
|
136
|
-
it(
|
|
137
|
-
const el = document.createElement(
|
|
138
|
-
vi.spyOn(el,
|
|
136
|
+
it('disconnects ResizeObserver on unmount', () => {
|
|
137
|
+
const el = document.createElement('div')
|
|
138
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
139
139
|
width: 0,
|
|
140
140
|
height: 0,
|
|
141
141
|
x: 0,
|
|
@@ -160,9 +160,9 @@ describe("useElementSize", () => {
|
|
|
160
160
|
expect(disconnectSpy).toHaveBeenCalled()
|
|
161
161
|
})
|
|
162
162
|
|
|
163
|
-
it(
|
|
164
|
-
const el = document.createElement(
|
|
165
|
-
vi.spyOn(el,
|
|
163
|
+
it('handles ResizeObserver callback with no entry', () => {
|
|
164
|
+
const el = document.createElement('div')
|
|
165
|
+
vi.spyOn(el, 'getBoundingClientRect').mockReturnValue({
|
|
166
166
|
width: 50,
|
|
167
167
|
height: 25,
|
|
168
168
|
x: 0,
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { useFocus } from
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { useFocus } from '../useFocus'
|
|
3
3
|
|
|
4
|
-
describe(
|
|
5
|
-
it(
|
|
4
|
+
describe('useFocus', () => {
|
|
5
|
+
it('initializes with focused=false', () => {
|
|
6
6
|
const { focused } = useFocus()
|
|
7
7
|
expect(focused()).toBe(false)
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
it(
|
|
10
|
+
it('sets focused to true on focus', () => {
|
|
11
11
|
const { focused, props } = useFocus()
|
|
12
12
|
props.onFocus()
|
|
13
13
|
expect(focused()).toBe(true)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
it(
|
|
16
|
+
it('sets focused to false on blur', () => {
|
|
17
17
|
const { focused, props } = useFocus()
|
|
18
18
|
props.onFocus()
|
|
19
19
|
expect(focused()).toBe(true)
|
|
@@ -21,7 +21,7 @@ describe("useFocus", () => {
|
|
|
21
21
|
expect(focused()).toBe(false)
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
it(
|
|
24
|
+
it('toggles focus state through multiple cycles', () => {
|
|
25
25
|
const { focused, props } = useFocus()
|
|
26
26
|
props.onFocus()
|
|
27
27
|
expect(focused()).toBe(true)
|
|
@@ -31,22 +31,22 @@ describe("useFocus", () => {
|
|
|
31
31
|
expect(focused()).toBe(true)
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
it(
|
|
34
|
+
it('calling blur when not focused is safe', () => {
|
|
35
35
|
const { focused, props } = useFocus()
|
|
36
36
|
props.onBlur()
|
|
37
37
|
expect(focused()).toBe(false)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it(
|
|
40
|
+
it('calling focus multiple times stays true', () => {
|
|
41
41
|
const { focused, props } = useFocus()
|
|
42
42
|
props.onFocus()
|
|
43
43
|
props.onFocus()
|
|
44
44
|
expect(focused()).toBe(true)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it(
|
|
47
|
+
it('returns props object with onFocus and onBlur', () => {
|
|
48
48
|
const { props } = useFocus()
|
|
49
|
-
expect(typeof props.onFocus).toBe(
|
|
50
|
-
expect(typeof props.onBlur).toBe(
|
|
49
|
+
expect(typeof props.onFocus).toBe('function')
|
|
50
|
+
expect(typeof props.onBlur).toBe('function')
|
|
51
51
|
})
|
|
52
52
|
})
|
|
@@ -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<() => void> = []
|
|
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 as () => void)
|
|
9
9
|
},
|
|
@@ -12,9 +12,9 @@ vi.mock("@pyreon/core", () => ({
|
|
|
12
12
|
},
|
|
13
13
|
}))
|
|
14
14
|
|
|
15
|
-
import { useFocusTrap } from
|
|
15
|
+
import { useFocusTrap } from '../useFocusTrap'
|
|
16
16
|
|
|
17
|
-
describe(
|
|
17
|
+
describe('useFocusTrap', () => {
|
|
18
18
|
let container: HTMLDivElement
|
|
19
19
|
let btn1: HTMLButtonElement
|
|
20
20
|
let btn2: HTMLButtonElement
|
|
@@ -23,10 +23,10 @@ describe("useFocusTrap", () => {
|
|
|
23
23
|
beforeEach(() => {
|
|
24
24
|
mountCallbacks = []
|
|
25
25
|
unmountCallbacks = []
|
|
26
|
-
container = document.createElement(
|
|
27
|
-
btn1 = document.createElement(
|
|
28
|
-
btn2 = document.createElement(
|
|
29
|
-
btn3 = document.createElement(
|
|
26
|
+
container = document.createElement('div')
|
|
27
|
+
btn1 = document.createElement('button')
|
|
28
|
+
btn2 = document.createElement('button')
|
|
29
|
+
btn3 = document.createElement('button')
|
|
30
30
|
container.append(btn1, btn2, btn3)
|
|
31
31
|
document.body.appendChild(container)
|
|
32
32
|
})
|
|
@@ -35,84 +35,84 @@ describe("useFocusTrap", () => {
|
|
|
35
35
|
document.body.removeChild(container)
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
it(
|
|
38
|
+
it('wraps focus from last to first on Tab', () => {
|
|
39
39
|
useFocusTrap(() => container)
|
|
40
40
|
mountCallbacks.forEach((cb) => {
|
|
41
41
|
cb()
|
|
42
42
|
})
|
|
43
43
|
|
|
44
44
|
btn3.focus()
|
|
45
|
-
const event = new KeyboardEvent(
|
|
46
|
-
const prevented = vi.spyOn(event,
|
|
45
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
|
46
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
47
47
|
document.dispatchEvent(event)
|
|
48
48
|
|
|
49
49
|
expect(prevented).toHaveBeenCalled()
|
|
50
50
|
expect(document.activeElement).toBe(btn1)
|
|
51
51
|
})
|
|
52
52
|
|
|
53
|
-
it(
|
|
53
|
+
it('wraps focus from first to last on Shift+Tab', () => {
|
|
54
54
|
useFocusTrap(() => container)
|
|
55
55
|
mountCallbacks.forEach((cb) => {
|
|
56
56
|
cb()
|
|
57
57
|
})
|
|
58
58
|
|
|
59
59
|
btn1.focus()
|
|
60
|
-
const event = new KeyboardEvent(
|
|
61
|
-
key:
|
|
60
|
+
const event = new KeyboardEvent('keydown', {
|
|
61
|
+
key: 'Tab',
|
|
62
62
|
shiftKey: true,
|
|
63
63
|
bubbles: true,
|
|
64
64
|
})
|
|
65
|
-
const prevented = vi.spyOn(event,
|
|
65
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
66
66
|
document.dispatchEvent(event)
|
|
67
67
|
|
|
68
68
|
expect(prevented).toHaveBeenCalled()
|
|
69
69
|
expect(document.activeElement).toBe(btn3)
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(
|
|
72
|
+
it('does not wrap when Tab on middle element', () => {
|
|
73
73
|
useFocusTrap(() => container)
|
|
74
74
|
mountCallbacks.forEach((cb) => {
|
|
75
75
|
cb()
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
btn2.focus()
|
|
79
|
-
const event = new KeyboardEvent(
|
|
80
|
-
const prevented = vi.spyOn(event,
|
|
79
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
|
80
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
81
81
|
document.dispatchEvent(event)
|
|
82
82
|
|
|
83
83
|
expect(prevented).not.toHaveBeenCalled()
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
it(
|
|
86
|
+
it('ignores non-Tab keys', () => {
|
|
87
87
|
useFocusTrap(() => container)
|
|
88
88
|
mountCallbacks.forEach((cb) => {
|
|
89
89
|
cb()
|
|
90
90
|
})
|
|
91
91
|
|
|
92
92
|
btn3.focus()
|
|
93
|
-
const event = new KeyboardEvent(
|
|
94
|
-
const prevented = vi.spyOn(event,
|
|
93
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
|
|
94
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
95
95
|
document.dispatchEvent(event)
|
|
96
96
|
|
|
97
97
|
expect(prevented).not.toHaveBeenCalled()
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
it(
|
|
100
|
+
it('does nothing when element is null', () => {
|
|
101
101
|
useFocusTrap(() => null)
|
|
102
102
|
mountCallbacks.forEach((cb) => {
|
|
103
103
|
cb()
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
-
const event = new KeyboardEvent(
|
|
107
|
-
const prevented = vi.spyOn(event,
|
|
106
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
|
107
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
108
108
|
document.dispatchEvent(event)
|
|
109
109
|
|
|
110
110
|
expect(prevented).not.toHaveBeenCalled()
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
it(
|
|
114
|
-
const emptyContainer = document.createElement(
|
|
115
|
-
emptyContainer.appendChild(document.createElement(
|
|
113
|
+
it('does nothing when container has no focusable children', () => {
|
|
114
|
+
const emptyContainer = document.createElement('div')
|
|
115
|
+
emptyContainer.appendChild(document.createElement('div'))
|
|
116
116
|
document.body.appendChild(emptyContainer)
|
|
117
117
|
|
|
118
118
|
useFocusTrap(() => emptyContainer)
|
|
@@ -120,16 +120,16 @@ describe("useFocusTrap", () => {
|
|
|
120
120
|
cb()
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
const event = new KeyboardEvent(
|
|
124
|
-
const prevented = vi.spyOn(event,
|
|
123
|
+
const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
|
124
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
125
125
|
document.dispatchEvent(event)
|
|
126
126
|
|
|
127
127
|
expect(prevented).not.toHaveBeenCalled()
|
|
128
128
|
document.body.removeChild(emptyContainer)
|
|
129
129
|
})
|
|
130
130
|
|
|
131
|
-
it(
|
|
132
|
-
const removeSpy = vi.spyOn(document,
|
|
131
|
+
it('cleans up event listener on unmount', () => {
|
|
132
|
+
const removeSpy = vi.spyOn(document, 'removeEventListener')
|
|
133
133
|
useFocusTrap(() => container)
|
|
134
134
|
mountCallbacks.forEach((cb) => {
|
|
135
135
|
cb()
|
|
@@ -138,23 +138,23 @@ describe("useFocusTrap", () => {
|
|
|
138
138
|
cb()
|
|
139
139
|
})
|
|
140
140
|
|
|
141
|
-
expect(removeSpy).toHaveBeenCalledWith(
|
|
141
|
+
expect(removeSpy).toHaveBeenCalledWith('keydown', expect.any(Function))
|
|
142
142
|
removeSpy.mockRestore()
|
|
143
143
|
})
|
|
144
144
|
|
|
145
|
-
it(
|
|
145
|
+
it('does not prevent default when Shift+Tab on non-first element', () => {
|
|
146
146
|
useFocusTrap(() => container)
|
|
147
147
|
mountCallbacks.forEach((cb) => {
|
|
148
148
|
cb()
|
|
149
149
|
})
|
|
150
150
|
|
|
151
151
|
btn2.focus()
|
|
152
|
-
const event = new KeyboardEvent(
|
|
153
|
-
key:
|
|
152
|
+
const event = new KeyboardEvent('keydown', {
|
|
153
|
+
key: 'Tab',
|
|
154
154
|
shiftKey: true,
|
|
155
155
|
bubbles: true,
|
|
156
156
|
})
|
|
157
|
-
const prevented = vi.spyOn(event,
|
|
157
|
+
const prevented = vi.spyOn(event, 'preventDefault')
|
|
158
158
|
document.dispatchEvent(event)
|
|
159
159
|
|
|
160
160
|
expect(prevented).not.toHaveBeenCalled()
|