@musecat/functionkit 1.1.0 → 1.2.0

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.
Files changed (91) hide show
  1. package/package.json +17 -1
  2. package/packages/cookie/cookie.shared.ts +10 -2
  3. package/packages/utils/floatingMotion.ts +1 -3
  4. package/.agents/references/components/ScrolltoTop.md +0 -36
  5. package/.agents/references/components/SwitchCase.md +0 -47
  6. package/.agents/references/components/ViewportPortal.md +0 -34
  7. package/.agents/references/cookie/cookie.md +0 -47
  8. package/.agents/references/datetime/dateTime.client.md +0 -42
  9. package/.agents/references/datetime/dateTime.server.md +0 -42
  10. package/.agents/references/datetime/dateTime.shared.md +0 -86
  11. package/.agents/references/hooks/useAvoidKeyboard.md +0 -29
  12. package/.agents/references/hooks/useCheckInvisible.md +0 -36
  13. package/.agents/references/hooks/useCheckScroll.md +0 -36
  14. package/.agents/references/hooks/useClientDateTime.md +0 -39
  15. package/.agents/references/hooks/useDebounce.md +0 -46
  16. package/.agents/references/hooks/useDebouncedCallback.md +0 -34
  17. package/.agents/references/hooks/useDoubleClick.md +0 -40
  18. package/.agents/references/hooks/useGeolocation.md +0 -37
  19. package/.agents/references/hooks/useHasMounted.md +0 -27
  20. package/.agents/references/hooks/useIntersectionObserver.md +0 -41
  21. package/.agents/references/hooks/useInterval.md +0 -44
  22. package/.agents/references/hooks/useKeyboardHeight.md +0 -33
  23. package/.agents/references/hooks/useKeyboardListNavigation.md +0 -58
  24. package/.agents/references/hooks/useLongPress.md +0 -48
  25. package/.agents/references/hooks/usePreservedCallback.md +0 -37
  26. package/.agents/references/hooks/usePreservedReference.md +0 -34
  27. package/.agents/references/hooks/useRefEffect.md +0 -35
  28. package/.agents/references/hooks/useRelativeDateTime.md +0 -35
  29. package/.agents/references/hooks/useTimeout.md +0 -42
  30. package/.agents/references/hooks/useToggleState.md +0 -39
  31. package/.agents/references/hooks/useViewportHeight.md +0 -26
  32. package/.agents/references/hooks/useViewportMatch.md +0 -32
  33. package/.agents/references/utils/browserStorage.md +0 -41
  34. package/.agents/references/utils/buildContext.md +0 -47
  35. package/.agents/references/utils/clipboardShare.md +0 -59
  36. package/.agents/references/utils/floatingMotion.md +0 -67
  37. package/.agents/references/utils/getDeviceInfo.md +0 -43
  38. package/.agents/references/utils/isEditableKeyboardTarget.md +0 -35
  39. package/.agents/references/utils/mergeRefs.md +0 -29
  40. package/.agents/references/utils/seen.md +0 -37
  41. package/.agents/references/utils/subscribeKeyboardHeight.md +0 -35
  42. package/.gitattributes +0 -2
  43. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  44. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -22
  45. package/.github/PULL_REQUEST_TEMPLATE.md +0 -22
  46. package/.github/workflows/ci.yml +0 -20
  47. package/.local/state/gh/device-id +0 -1
  48. package/.prettierrc +0 -8
  49. package/AGENTS.md +0 -119
  50. package/CODE_OF_CONDUCT.md +0 -55
  51. package/CONTRIBUTING.md +0 -39
  52. package/biome.json +0 -25
  53. package/tests/components/ScrolltoTop.test.tsx +0 -15
  54. package/tests/components/SwitchCase.test.tsx +0 -33
  55. package/tests/components/ViewportPortal.test.tsx +0 -46
  56. package/tests/cookie/cookie.test.ts +0 -192
  57. package/tests/datetime/datetime.test.ts +0 -461
  58. package/tests/hooks/useAvoidKeyboard.test.ts +0 -43
  59. package/tests/hooks/useCheckInvisible.test.ts +0 -59
  60. package/tests/hooks/useCheckScroll.test.ts +0 -23
  61. package/tests/hooks/useClientDateTime.test.ts +0 -12
  62. package/tests/hooks/useDebounce.test.ts +0 -96
  63. package/tests/hooks/useDebouncedCallback.test.ts +0 -63
  64. package/tests/hooks/useDoubleClick.test.ts +0 -82
  65. package/tests/hooks/useGeolocation.test.ts +0 -201
  66. package/tests/hooks/useHasMounted.test.ts +0 -10
  67. package/tests/hooks/useIntersectionObserver.test.ts +0 -78
  68. package/tests/hooks/useInterval.test.ts +0 -56
  69. package/tests/hooks/useKeyboardHeight.test.ts +0 -10
  70. package/tests/hooks/useKeyboardListNavigation.test.ts +0 -404
  71. package/tests/hooks/useLongPress.test.ts +0 -214
  72. package/tests/hooks/usePreservedCallback.test.ts +0 -33
  73. package/tests/hooks/usePreservedReference.test.ts +0 -42
  74. package/tests/hooks/useRefEffect.test.ts +0 -67
  75. package/tests/hooks/useRelativeDateTime.test.ts +0 -21
  76. package/tests/hooks/useTimeout.test.ts +0 -72
  77. package/tests/hooks/useToggleState.test.ts +0 -43
  78. package/tests/hooks/useViewportHeight.test.ts +0 -32
  79. package/tests/hooks/useViewportMatch.test.ts +0 -35
  80. package/tests/setup.ts +0 -9
  81. package/tests/utils/browserStorage.test.ts +0 -114
  82. package/tests/utils/buildContext.test.tsx +0 -26
  83. package/tests/utils/checkDevice.test.ts +0 -140
  84. package/tests/utils/clipboardShare.test.ts +0 -152
  85. package/tests/utils/floatingMotion.test.ts +0 -135
  86. package/tests/utils/keyboardTarget.test.ts +0 -85
  87. package/tests/utils/mergeRefs.test.ts +0 -50
  88. package/tests/utils/seen.test.ts +0 -40
  89. package/tests/utils/subscribeKeyboardHeight.test.ts +0 -172
  90. package/tsconfig.json +0 -18
  91. package/vitest.config.ts +0 -22
@@ -1,152 +0,0 @@
1
- import { afterEach, describe, expect, test, vi } from "vitest";
2
- import { NavigatorClipboard, NavigatorShare } from "@/packages/utils/clipboardShare";
3
-
4
- describe("NavigatorClipboard", () => {
5
- test("resolves with success: true on write", async () => {
6
- Object.defineProperty(navigator, "clipboard", {
7
- value: { writeText: vi.fn(() => Promise.resolve()) },
8
- writable: true,
9
- configurable: true,
10
- });
11
-
12
- const result = await NavigatorClipboard({ text: "hello" });
13
- expect(result).toEqual({ success: true });
14
- });
15
-
16
- test("resolves with success: false on failure", async () => {
17
- Object.defineProperty(navigator, "clipboard", {
18
- value: { writeText: vi.fn(() => Promise.reject(new Error("denied"))) },
19
- writable: true,
20
- configurable: true,
21
- });
22
-
23
- const result = await NavigatorClipboard({ text: "hello" });
24
- expect(result).toEqual({ success: false });
25
- });
26
- });
27
-
28
- describe("NavigatorShare", () => {
29
- afterEach(() => {
30
- vi.restoreAllMocks();
31
- });
32
-
33
- test("uses Web Share API when available", async () => {
34
- Object.defineProperty(navigator, "share", {
35
- value: vi.fn(() => Promise.resolve()),
36
- writable: true,
37
- configurable: true,
38
- });
39
- Object.defineProperty(navigator, "clipboard", {
40
- value: { writeText: vi.fn() },
41
- writable: true,
42
- configurable: true,
43
- });
44
-
45
- const result = await NavigatorShare({
46
- title: "Test",
47
- text: "Hello",
48
- link: "https://example.com",
49
- });
50
- expect(result).toEqual({ success: true, method: "share" });
51
- });
52
-
53
- test("falls back to clipboard when share is unavailable", async () => {
54
- Object.defineProperty(navigator, "share", {
55
- value: undefined,
56
- writable: true,
57
- configurable: true,
58
- });
59
- Object.defineProperty(navigator, "clipboard", {
60
- value: { writeText: vi.fn(() => Promise.resolve()) },
61
- writable: true,
62
- configurable: true,
63
- });
64
-
65
- const result = await NavigatorShare({
66
- title: "Test",
67
- text: "Hello",
68
- link: "https://example.com",
69
- });
70
- expect(result).toEqual({ success: true, method: "clipboard" });
71
- });
72
-
73
- test("handles AbortError from Web Share API", async () => {
74
- Object.defineProperty(navigator, "share", {
75
- value: vi.fn(() => Promise.reject(new DOMException("Aborted", "AbortError"))),
76
- writable: true,
77
- configurable: true,
78
- });
79
- Object.defineProperty(navigator, "clipboard", {
80
- value: { writeText: vi.fn() },
81
- writable: true,
82
- configurable: true,
83
- });
84
-
85
- const result = await NavigatorShare({
86
- title: "Test",
87
- text: "Hello",
88
- link: "https://example.com",
89
- });
90
- expect(result).toEqual({ success: false, method: "share" });
91
- });
92
-
93
- test("falls back to clipboard on non-AbortError share rejection", async () => {
94
- Object.defineProperty(navigator, "share", {
95
- value: vi.fn(() => Promise.reject(new Error("NotAllowedError"))),
96
- writable: true,
97
- configurable: true,
98
- });
99
- Object.defineProperty(navigator, "clipboard", {
100
- value: { writeText: vi.fn(() => Promise.resolve()) },
101
- writable: true,
102
- configurable: true,
103
- });
104
-
105
- const result = await NavigatorShare({
106
- title: "Test",
107
- text: "Hello",
108
- link: "https://example.com",
109
- });
110
- expect(result).toEqual({ success: true, method: "clipboard" });
111
- });
112
-
113
- test("clipboard fallback rejects with unsupported", async () => {
114
- Object.defineProperty(navigator, "share", {
115
- value: vi.fn(() => Promise.reject(new Error("share failed"))),
116
- writable: true,
117
- configurable: true,
118
- });
119
- Object.defineProperty(navigator, "clipboard", {
120
- value: { writeText: vi.fn(() => Promise.reject(new Error("clipboard denied"))) },
121
- writable: true,
122
- configurable: true,
123
- });
124
-
125
- const result = await NavigatorShare({
126
- title: "Test",
127
- text: "Hello",
128
- link: "https://example.com",
129
- });
130
- expect(result).toEqual({ success: false, method: "unsupported" });
131
- });
132
-
133
- test("returns unsupported when neither share nor clipboard", async () => {
134
- Object.defineProperty(navigator, "share", {
135
- value: undefined,
136
- writable: true,
137
- configurable: true,
138
- });
139
- Object.defineProperty(navigator, "clipboard", {
140
- value: undefined,
141
- writable: true,
142
- configurable: true,
143
- });
144
-
145
- const result = await NavigatorShare({
146
- title: "Test",
147
- text: "Hello",
148
- link: "https://example.com",
149
- });
150
- expect(result).toEqual({ success: false, method: "unsupported" });
151
- });
152
- });
@@ -1,135 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import {
3
- getFloatingHiddenTransform,
4
- getFloatingMotionPreset,
5
- getFloatingTransformOrigin,
6
- } from "@/packages/utils/floatingMotion";
7
-
8
- describe("getFloatingMotionPreset", () => {
9
- test("returns anchored preset", () => {
10
- const preset = getFloatingMotionPreset("anchored");
11
- expect(preset.enterMs).toBe(230);
12
- expect(preset.exitMs).toBe(170);
13
- });
14
-
15
- test("returns modal-center preset", () => {
16
- const preset = getFloatingMotionPreset("modal-center");
17
- expect(preset.enterMs).toBe(340);
18
- expect(preset.exitMs).toBe(260);
19
- });
20
-
21
- test("returns center-selected preset", () => {
22
- const preset = getFloatingMotionPreset("center-selected");
23
- expect(preset.enterMs).toBe(190);
24
- expect(preset.exitMs).toBe(140);
25
- });
26
-
27
- test("returns mobile-sheet preset", () => {
28
- const preset = getFloatingMotionPreset("mobile-sheet");
29
- expect(preset.enterMs).toBe(360);
30
- expect(preset.exitMs).toBe(280);
31
- });
32
- });
33
-
34
- describe("getFloatingTransformOrigin", () => {
35
- test("returns default when no placement", () => {
36
- expect(getFloatingTransformOrigin()).toBe("top center");
37
- });
38
-
39
- test("returns correct origin for top-left", () => {
40
- expect(getFloatingTransformOrigin("top-left")).toBe("bottom left");
41
- });
42
-
43
- test("returns correct origin for bottom-right", () => {
44
- expect(getFloatingTransformOrigin("bottom-right")).toBe("top right");
45
- });
46
-
47
- test("returns correct origin for middle-center", () => {
48
- expect(getFloatingTransformOrigin("middle-center")).toBe("center center");
49
- });
50
-
51
- test("returns correct origin for top-center", () => {
52
- expect(getFloatingTransformOrigin("top-center")).toBe("bottom center");
53
- });
54
-
55
- test("returns correct origin for top-right", () => {
56
- expect(getFloatingTransformOrigin("top-right")).toBe("bottom right");
57
- });
58
-
59
- test("returns correct origin for middle-left", () => {
60
- expect(getFloatingTransformOrigin("middle-left")).toBe("center left");
61
- });
62
-
63
- test("returns correct origin for middle-right", () => {
64
- expect(getFloatingTransformOrigin("middle-right")).toBe("center right");
65
- });
66
-
67
- test("returns correct origin for bottom-left", () => {
68
- expect(getFloatingTransformOrigin("bottom-left")).toBe("top left");
69
- });
70
-
71
- test("returns correct origin for bottom-center", () => {
72
- expect(getFloatingTransformOrigin("bottom-center")).toBe("top center");
73
- });
74
- });
75
-
76
- describe("getFloatingHiddenTransform", () => {
77
- test("returns mobile-sheet transform", () => {
78
- expect(getFloatingHiddenTransform({ mode: "mobile-sheet" })).toBe(
79
- "translateY(1.8rem) scale(1)",
80
- );
81
- });
82
-
83
- test("returns modal-center transform", () => {
84
- expect(getFloatingHiddenTransform({ mode: "modal-center" })).toBe(
85
- "translateY(.8rem) scale(.94)",
86
- );
87
- });
88
-
89
- test("returns center-selected transform", () => {
90
- expect(getFloatingHiddenTransform({ mode: "center-selected" })).toBe(
91
- "translateY(.45rem) scale(.99)",
92
- );
93
- });
94
-
95
- test("anchored with no placement", () => {
96
- expect(getFloatingHiddenTransform({ mode: "anchored" })).toBe("translateY(.4rem) scale(.975)");
97
- });
98
-
99
- test("anchored with top placement", () => {
100
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "top-left" })).toBe(
101
- "translateY(.4rem) scale(.975)",
102
- );
103
- });
104
-
105
- test("anchored with bottom placement", () => {
106
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "bottom-center" })).toBe(
107
- "translateY(-.4rem) scale(.975)",
108
- );
109
- });
110
-
111
- test("anchored with middle-left placement", () => {
112
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-left" })).toBe(
113
- "translateX(.4rem) scale(.975)",
114
- );
115
- });
116
-
117
- test("anchored with middle-right placement", () => {
118
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-right" })).toBe(
119
- "translateX(-.4rem) scale(.975)",
120
- );
121
- });
122
-
123
- test("anchored with middle-center placement falls through to default", () => {
124
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle-center" })).toBe(
125
- "translateY(.4rem) scale(.975)",
126
- );
127
- });
128
-
129
- test("anchored with middle placement and undefined col falls through", () => {
130
- // @ts-expect-error - testing unexpected placement fallthrough
131
- expect(getFloatingHiddenTransform({ mode: "anchored", placement: "middle" })).toBe(
132
- "translateY(.4rem) scale(.975)",
133
- );
134
- });
135
- });
@@ -1,85 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { isEditableKeyboardTarget } from "@/packages/utils/keyboardTarget";
3
-
4
- describe("isEditableKeyboardTarget", () => {
5
- test("returns true for contentEditable", () => {
6
- const el = document.createElement("div");
7
- Object.defineProperty(el, "isContentEditable", { value: true });
8
- expect(isEditableKeyboardTarget(el)).toBe(true);
9
- });
10
-
11
- test("returns true for TEXTAREA", () => {
12
- const el = document.createElement("textarea");
13
- expect(isEditableKeyboardTarget(el)).toBe(true);
14
- });
15
-
16
- test("returns true for text INPUT", () => {
17
- const el = document.createElement("input");
18
- el.type = "text";
19
- expect(isEditableKeyboardTarget(el)).toBe(true);
20
- });
21
-
22
- test("returns false for button INPUT", () => {
23
- const el = document.createElement("input");
24
- el.type = "button";
25
- expect(isEditableKeyboardTarget(el)).toBe(false);
26
- });
27
-
28
- test("returns false for checkbox INPUT", () => {
29
- const el = document.createElement("input");
30
- el.type = "checkbox";
31
- expect(isEditableKeyboardTarget(el)).toBe(false);
32
- });
33
-
34
- test("returns false for radio INPUT", () => {
35
- const el = document.createElement("input");
36
- el.type = "radio";
37
- expect(isEditableKeyboardTarget(el)).toBe(false);
38
- });
39
-
40
- test("returns false for regular div", () => {
41
- const el = document.createElement("div");
42
- expect(isEditableKeyboardTarget(el)).toBe(false);
43
- });
44
-
45
- test("returns true for SELECT element", () => {
46
- const el = document.createElement("select");
47
- expect(isEditableKeyboardTarget(el)).toBe(true);
48
- });
49
-
50
- test("returns true for email INPUT", () => {
51
- const el = document.createElement("input");
52
- el.type = "email";
53
- expect(isEditableKeyboardTarget(el)).toBe(true);
54
- });
55
-
56
- test("returns true for url INPUT", () => {
57
- const el = document.createElement("input");
58
- el.type = "url";
59
- expect(isEditableKeyboardTarget(el)).toBe(true);
60
- });
61
-
62
- test("returns true for number INPUT", () => {
63
- const el = document.createElement("input");
64
- el.type = "number";
65
- expect(isEditableKeyboardTarget(el)).toBe(true);
66
- });
67
-
68
- test("returns true for password INPUT", () => {
69
- const el = document.createElement("input");
70
- el.type = "password";
71
- expect(isEditableKeyboardTarget(el)).toBe(true);
72
- });
73
-
74
- test("returns true for search INPUT", () => {
75
- const el = document.createElement("input");
76
- el.type = "search";
77
- expect(isEditableKeyboardTarget(el)).toBe(true);
78
- });
79
-
80
- test("returns true for tel INPUT", () => {
81
- const el = document.createElement("input");
82
- el.type = "tel";
83
- expect(isEditableKeyboardTarget(el)).toBe(true);
84
- });
85
- });
@@ -1,50 +0,0 @@
1
- import { describe, expect, test, vi } from "vitest";
2
- import { mergeRefs } from "@/packages/utils/mergeRefs";
3
-
4
- describe("mergeRefs", () => {
5
- test("merges multiple refs", () => {
6
- const ref1 = vi.fn();
7
- const ref2 = { current: null };
8
- const ref3 = null;
9
-
10
- const merged = mergeRefs(ref1, ref2, ref3);
11
- const element = document.createElement("div");
12
-
13
- merged(element as any);
14
-
15
- expect(ref1).toHaveBeenCalledWith(element);
16
- expect(ref2.current).toBe(element);
17
- });
18
-
19
- test("handles all null refs", () => {
20
- const merged = mergeRefs(null, undefined, null);
21
- const element = document.createElement("div");
22
- expect(() => merged(element as any)).not.toThrow();
23
- });
24
-
25
- test("handles empty refs array", () => {
26
- const merged = mergeRefs();
27
- const element = document.createElement("div");
28
- expect(() => merged(element as any)).not.toThrow();
29
- });
30
-
31
- test("handles all callback refs", () => {
32
- const ref1 = vi.fn();
33
- const ref2 = vi.fn();
34
- const merged = mergeRefs(ref1, ref2);
35
- const element = document.createElement("div");
36
- merged(element as any);
37
- expect(ref1).toHaveBeenCalledWith(element);
38
- expect(ref2).toHaveBeenCalledWith(element);
39
- });
40
-
41
- test("handles all object refs", () => {
42
- const ref1 = { current: null };
43
- const ref2 = { current: null };
44
- const merged = mergeRefs(ref1, ref2);
45
- const element = document.createElement("div");
46
- merged(element as any);
47
- expect(ref1.current).toBe(element);
48
- expect(ref2.current).toBe(element);
49
- });
50
- });
@@ -1,40 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { buildSeenValue, hasSeenKey, parseSeen, SEEN_STORAGE_KEY } from "@/packages/utils/seen";
3
-
4
- describe("seen", () => {
5
- test("SEEN_STORAGE_KEY", () => {
6
- expect(SEEN_STORAGE_KEY).toBe("seen");
7
- });
8
-
9
- test("parseSeen", () => {
10
- expect(parseSeen(null)).toEqual({});
11
- expect(parseSeen('{"a":true}')).toEqual({ a: true });
12
- expect(parseSeen("invalid json")).toEqual({});
13
- expect(parseSeen("[]")).toEqual({});
14
- });
15
-
16
- test("hasSeenKey", () => {
17
- expect(hasSeenKey('{"a":true}', "a")).toBe(true);
18
- expect(hasSeenKey('{"a":true}', "b")).toBe(false);
19
- expect(hasSeenKey(null, "a")).toBe(false);
20
- });
21
-
22
- test("buildSeenValue", () => {
23
- const result = buildSeenValue('{"a":true}', "b");
24
- expect(JSON.parse(result)).toEqual({ a: true, b: true });
25
- });
26
-
27
- test("parseSeen empty string returns {}", () => {
28
- expect(parseSeen("")).toEqual({});
29
- });
30
-
31
- test("buildSeenValue with null raw creates fresh object", () => {
32
- const result = buildSeenValue(null, "key");
33
- expect(JSON.parse(result)).toEqual({ key: true });
34
- });
35
-
36
- test("buildSeenValue overrides existing key to true", () => {
37
- const result = buildSeenValue('{"key":false}', "key");
38
- expect(JSON.parse(result)).toEqual({ key: true });
39
- });
40
- });
@@ -1,172 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
2
- import { subscribeKeyboardHeight } from "@/packages/utils/subscribeKeyboardHeight";
3
-
4
- describe("subscribeKeyboardHeight", () => {
5
- let visualViewport: any;
6
-
7
- beforeEach(() => {
8
- visualViewport = {
9
- height: 600,
10
- width: 390,
11
- offsetTop: 0,
12
- addEventListener: vi.fn(),
13
- removeEventListener: vi.fn(),
14
- };
15
- Object.defineProperty(window, "visualViewport", {
16
- value: visualViewport,
17
- writable: true,
18
- configurable: true,
19
- });
20
- Object.defineProperty(window, "innerHeight", {
21
- value: 800,
22
- writable: true,
23
- configurable: true,
24
- });
25
- });
26
-
27
- afterEach(() => {
28
- vi.useRealTimers();
29
- });
30
-
31
- test("subscribes to resize events", () => {
32
- const callback = vi.fn();
33
- subscribeKeyboardHeight({ callback });
34
- expect(visualViewport.addEventListener).toHaveBeenCalledWith("resize", expect.any(Function));
35
- expect(visualViewport.addEventListener).toHaveBeenCalledWith("scroll", expect.any(Function));
36
- });
37
-
38
- test("calls immediately when immediate is true", () => {
39
- vi.useFakeTimers();
40
- const callback = vi.fn();
41
- subscribeKeyboardHeight({ callback, immediate: true });
42
- vi.advanceTimersByTime(16);
43
- expect(callback).toHaveBeenCalled();
44
- });
45
-
46
- test("runs synchronously when throttleMs is 0", () => {
47
- const callback = vi.fn();
48
- subscribeKeyboardHeight({ callback, throttleMs: 0, immediate: true });
49
- expect(callback).toHaveBeenCalled();
50
- });
51
-
52
- test("skips resize when visualViewport is null", () => {
53
- const origVv = window.visualViewport;
54
- Object.defineProperty(window, "visualViewport", {
55
- value: undefined,
56
- writable: true,
57
- configurable: true,
58
- });
59
- const callback = vi.fn();
60
- subscribeKeyboardHeight({ callback, immediate: true });
61
- expect(callback).not.toHaveBeenCalled();
62
- Object.defineProperty(window, "visualViewport", {
63
- value: origVv,
64
- writable: true,
65
- configurable: true,
66
- });
67
- });
68
-
69
- test("does not call callback when height unchanged", () => {
70
- const callback = vi.fn();
71
- subscribeKeyboardHeight({ callback, throttleMs: 0, immediate: true });
72
- expect(callback).toHaveBeenCalledTimes(1);
73
-
74
- // Trigger resize with the same keyboard height
75
- const handler = visualViewport.addEventListener.mock.calls.find(
76
- ([event]: [string]) => event === "resize",
77
- )?.[1] as () => void;
78
- if (handler) handler();
79
-
80
- // Same diff (innerHeight 800 - visualViewport.height 600 = 200), so notify(200) again
81
- // But lastHeight is already 200, so height !== lastHeight is false
82
- expect(callback).toHaveBeenCalledTimes(1);
83
- });
84
-
85
- test("handles no keyboard scenario (diff <= 0)", () => {
86
- const origInnerHeight = window.innerHeight;
87
- // First, call with keyboard raised (diff=200) so lastHeight becomes 200
88
- const callback = vi.fn();
89
- subscribeKeyboardHeight({ callback, throttleMs: 0, immediate: true });
90
- expect(callback).toHaveBeenCalledWith(200);
91
- // Now change innerHeight to match visualViewport.height (no keyboard)
92
- Object.defineProperty(window, "innerHeight", {
93
- value: 600,
94
- writable: true,
95
- configurable: true,
96
- });
97
- // Trigger resize handler
98
- const resizeHandler = visualViewport.addEventListener.mock.calls.find(
99
- ([event]: [string]) => event === "resize",
100
- )?.[1] as () => void;
101
- resizeHandler();
102
- // diff = 600 - 600 = 0 → notify(0) → 0 !== 200 → callback(0)
103
- expect(callback).toHaveBeenCalledWith(0);
104
- Object.defineProperty(window, "innerHeight", {
105
- value: origInnerHeight,
106
- writable: true,
107
- configurable: true,
108
- });
109
- });
110
-
111
- test("throttles rapid resize events", () => {
112
- vi.useFakeTimers();
113
- const callback = vi.fn();
114
- subscribeKeyboardHeight({ callback, throttleMs: 50 });
115
-
116
- // Simulate two rapid resize events
117
- visualViewport.height = 700;
118
- visualViewport.addEventListener.mock.calls.forEach(([event, handler]: [string, () => void]) => {
119
- if (event === "resize") handler();
120
- });
121
-
122
- // Change height again and fire again
123
- visualViewport.height = 650;
124
- visualViewport.addEventListener.mock.calls.forEach(([event, handler]: [string, () => void]) => {
125
- if (event === "resize") handler();
126
- });
127
-
128
- // Only the last height should be reported after throttle
129
- vi.advanceTimersByTime(50);
130
- expect(callback).toHaveBeenCalledTimes(1);
131
- });
132
-
133
- test("returns unsubscribe function", () => {
134
- const callback = vi.fn();
135
- const { unsubscribe } = subscribeKeyboardHeight({ callback });
136
- unsubscribe();
137
- expect(visualViewport.removeEventListener).toHaveBeenCalled();
138
- });
139
-
140
- test("does not call callback after unsubscribe with throttle > 0", () => {
141
- vi.useFakeTimers();
142
- const callback = vi.fn();
143
- const { unsubscribe } = subscribeKeyboardHeight({ callback, throttleMs: 50 });
144
-
145
- // Trigger resize, which schedules a setTimeout
146
- visualViewport.height = 400;
147
- visualViewport.dispatchEvent?.(new Event("resize"));
148
- if (visualViewport.addEventListener.mock.calls.length > 0) {
149
- const handler = visualViewport.addEventListener.mock.calls[0][1];
150
- handler?.();
151
- }
152
-
153
- unsubscribe();
154
- vi.advanceTimersByTime(100);
155
- expect(callback).not.toHaveBeenCalled();
156
- });
157
-
158
- test("does not call callback after unsubscribe with throttle === 0", () => {
159
- vi.useFakeTimers();
160
- const callback = vi.fn();
161
- const { unsubscribe } = subscribeKeyboardHeight({ callback, throttleMs: 0 });
162
-
163
- unsubscribe();
164
-
165
- visualViewport.height = 400;
166
- if (visualViewport.addEventListener.mock.calls.length > 0) {
167
- const handler = visualViewport.addEventListener.mock.calls[0][1];
168
- handler?.();
169
- }
170
- expect(callback).not.toHaveBeenCalled();
171
- });
172
- });
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2017",
4
- "lib": ["dom", "dom.iterable", "esnext"],
5
- "allowJs": true,
6
- "skipLibCheck": true,
7
- "strict": true,
8
- "noEmit": true,
9
- "esModuleInterop": true,
10
- "module": "esnext",
11
- "moduleResolution": "bundler",
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "jsx": "react-jsx",
15
- "incremental": true
16
- },
17
- "include": ["**/*.ts", "**/*.tsx"]
18
- }
package/vitest.config.ts DELETED
@@ -1,22 +0,0 @@
1
- import path from "node:path";
2
- import { defineConfig } from "vitest/config";
3
-
4
- export default defineConfig({
5
- test: {
6
- environment: "jsdom",
7
- setupFiles: ["./tests/setup.ts"],
8
- include: ["tests/**/*.test.{ts,tsx}"],
9
- globals: true,
10
- coverage: {
11
- provider: "v8",
12
- include: ["packages/**/*.{ts,tsx}"],
13
- exclude: ["packages/**/*.types.ts"],
14
- reporter: ["text", "lcov"],
15
- },
16
- },
17
- resolve: {
18
- alias: {
19
- "@": path.resolve(__dirname, "./"),
20
- },
21
- },
22
- });