@praxisjs/composables 0.1.4 → 1.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # @praxisjs/composables
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 3372878: Migrate all packages from functional APIs to a decorator-first design.
8
+
9
+ **`@praxisjs/core`**
10
+
11
+ - Added `Composable` abstract base class for building class-based composables
12
+ - Removed `resource`, `createResource`, `Resource`, `ResourceStatus`, `ResourceOptions` from public exports — use `@Resource` from `@praxisjs/decorators` instead
13
+
14
+ **`@praxisjs/motion`**
15
+
16
+ - Replaced `useMotion`, `tween`, `spring`, `createTransition`, `Animate`, `easings`, `resolveEasing` with `@Tween` and `@Spring` decorators
17
+
18
+ **`@praxisjs/di`**
19
+
20
+ - Replaced `useService` and `createScope` with a `@Scope` decorator
21
+ - Renamed exported type `Scope` to `ScopeType` to free the name for the new decorator
22
+
23
+ **`@praxisjs/fsm`**
24
+
25
+ - Removed `createMachine` — use the `@StateMachine` and `@Transition` decorators directly
26
+
27
+ **`@praxisjs/router`**
28
+
29
+ - Removed `createRouter`, `lazy`, `useRouter`, `useParams`, `useQuery`, `useLocation`
30
+ - Added `@RouterConfig`, `@Lazy`, `@InjectRouter`, `@Params`, `@Query`, `@Location` decorators
31
+
32
+ **`@praxisjs/store`**
33
+
34
+ - Removed `createStore` — use the `@Store` and `@UseStore` decorators directly
35
+
36
+ **`@praxisjs/composables`**
37
+
38
+ - Replaced all `use*` composable functions with class-based composables extending `Composable`:
39
+ `WindowSize`, `ScrollPosition`, `ElementSize`, `Intersection`, `Focus`, `MediaQuery`, `ColorScheme`, `Mouse`, `KeyCombo`, `Idle`, `Clipboard`, `Geolocation`, `TimeAgo`, `Pagination`
40
+
41
+ **`@praxisjs/concurrent`**
42
+
43
+ - Removed `task`, `queue`, `pool` and their instance types — use `@Task`, `@Queue`, `@Pool` decorators instead
44
+
45
+ ### Patch Changes
46
+
47
+ - Updated dependencies [3372878]
48
+ - @praxisjs/core@1.0.0
49
+
50
+ ## 0.1.5
51
+
52
+ ### Patch Changes
53
+
54
+ - Updated dependencies [d11a10a]
55
+ - @praxisjs/core@0.4.2
56
+
3
57
  ## 0.1.4
4
58
 
5
59
  ### Patch Changes
@@ -1,94 +1,187 @@
1
1
  // @vitest-environment jsdom
2
- import { describe, it, expect, vi } from "vitest";
3
- import { useMediaQuery, useColorScheme, useMouse, useKeyCombo, useIdle } from "../browser";
4
- // ---------- useMediaQuery ----------
5
- describe("useMediaQuery", () => {
6
- it("returns computed that reflects matchMedia result", () => {
7
- Object.defineProperty(window, "matchMedia", {
8
- writable: true,
9
- value: vi.fn((query) => ({
10
- matches: query === "(prefers-color-scheme: dark)",
11
- addEventListener: vi.fn(),
12
- removeEventListener: vi.fn(),
13
- })),
14
- });
15
- const isDark = useMediaQuery("(prefers-color-scheme: dark)");
16
- expect(isDark()).toBe(true);
17
- const isLight = useMediaQuery("(prefers-color-scheme: light)");
18
- expect(isLight()).toBe(false);
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import { MediaQuery, ColorScheme, Mouse, KeyCombo, Idle } from "../browser";
4
+ function mockMatchMedia(matches, onAdd) {
5
+ const mql = {
6
+ matches,
7
+ addEventListener: onAdd ?? vi.fn(),
8
+ removeEventListener: vi.fn(),
9
+ };
10
+ vi.stubGlobal("matchMedia", () => mql);
11
+ return mql;
12
+ }
13
+ // ── MediaQuery ────────────────────────────────────────────────────────────────
14
+ describe("MediaQuery", () => {
15
+ beforeEach(() => { vi.restoreAllMocks(); });
16
+ it("reads initial match state", () => {
17
+ mockMatchMedia(true);
18
+ const mq = new MediaQuery("(min-width: 768px)");
19
+ const { matches } = mq.setup();
20
+ expect(matches()).toBe(true);
21
+ });
22
+ it("updates matches when change event fires", () => {
23
+ let changeHandler;
24
+ mockMatchMedia(false, (_, fn) => { changeHandler = fn; });
25
+ const mq = new MediaQuery("(min-width: 768px)");
26
+ const { matches } = mq.setup();
27
+ expect(matches()).toBe(false);
28
+ changeHandler({ matches: true });
29
+ expect(matches()).toBe(true);
30
+ });
31
+ it("removes event listener on unmount", () => {
32
+ const mql = mockMatchMedia(false);
33
+ const mq = new MediaQuery("(min-width: 768px)");
34
+ mq.setup();
35
+ mq.onUnmount();
36
+ expect(mql.removeEventListener).toHaveBeenCalledWith("change", expect.any(Function));
37
+ });
38
+ it("onUnmount is safe to call without setup (covers false branch)", () => {
39
+ const mq = new MediaQuery("(min-width: 768px)");
40
+ expect(() => mq.onUnmount()).not.toThrow();
19
41
  });
20
42
  });
21
- // ---------- useColorScheme ----------
22
- describe("useColorScheme", () => {
23
- it("isDark and isLight are complementary", () => {
24
- Object.defineProperty(window, "matchMedia", {
25
- writable: true,
26
- value: vi.fn(() => ({
27
- matches: true,
28
- addEventListener: vi.fn(),
29
- removeEventListener: vi.fn(),
30
- })),
31
- });
32
- const { isDark, isLight } = useColorScheme();
43
+ // ── ColorScheme ───────────────────────────────────────────────────────────────
44
+ describe("ColorScheme", () => {
45
+ beforeEach(() => { vi.restoreAllMocks(); });
46
+ it("exposes isDark and isLight as computed inverses", () => {
47
+ let changeHandler;
48
+ mockMatchMedia(false, (_, fn) => { changeHandler = fn; });
49
+ const cs = new ColorScheme();
50
+ const { isDark, isLight } = cs.setup();
51
+ expect(isDark()).toBe(false);
52
+ expect(isLight()).toBe(true);
53
+ changeHandler({ matches: true });
33
54
  expect(isDark()).toBe(true);
34
55
  expect(isLight()).toBe(false);
35
56
  });
57
+ it("removes event listener on unmount", () => {
58
+ const mql = mockMatchMedia(false);
59
+ const cs = new ColorScheme();
60
+ cs.setup();
61
+ cs.onUnmount();
62
+ expect(mql.removeEventListener).toHaveBeenCalledWith("change", expect.any(Function));
63
+ });
64
+ it("onUnmount is safe to call without setup (covers false branch)", () => {
65
+ const cs = new ColorScheme();
66
+ expect(() => cs.onUnmount()).not.toThrow();
67
+ });
36
68
  });
37
- // ---------- useMouse ----------
38
- describe("useMouse", () => {
69
+ // ── Mouse ─────────────────────────────────────────────────────────────────────
70
+ describe("Mouse", () => {
39
71
  it("starts at (0, 0)", () => {
40
- const { x, y } = useMouse();
72
+ const mouse = new Mouse();
73
+ const { x, y } = mouse.setup();
41
74
  expect(x()).toBe(0);
42
75
  expect(y()).toBe(0);
43
76
  });
44
- it("updates on mousemove event", () => {
45
- const { x, y } = useMouse();
46
- window.dispatchEvent(new MouseEvent("mousemove", { clientX: 100, clientY: 200 }));
47
- expect(x()).toBe(100);
48
- expect(y()).toBe(200);
77
+ it("updates x and y on mousemove", () => {
78
+ const mouse = new Mouse();
79
+ const { x, y } = mouse.setup();
80
+ window.dispatchEvent(new MouseEvent("mousemove", { clientX: 120, clientY: 80 }));
81
+ expect(x()).toBe(120);
82
+ expect(y()).toBe(80);
83
+ });
84
+ it("removes listener on unmount", () => {
85
+ const remove = vi.spyOn(window, "removeEventListener");
86
+ const mouse = new Mouse();
87
+ mouse.setup();
88
+ mouse.onUnmount();
89
+ expect(remove).toHaveBeenCalledWith("mousemove", expect.any(Function));
49
90
  });
50
91
  });
51
- // ---------- useKeyCombo ----------
52
- describe("useKeyCombo", () => {
53
- it("starts as false", () => {
54
- const pressed = useKeyCombo("ctrl+s");
92
+ // ── KeyCombo ──────────────────────────────────────────────────────────────────
93
+ describe("KeyCombo", () => {
94
+ it("starts as not pressed", () => {
95
+ const kc = new KeyCombo("ctrl+s");
96
+ const { pressed } = kc.setup();
55
97
  expect(pressed()).toBe(false);
56
98
  });
57
- it("becomes true on matching keydown", () => {
58
- const pressed = useKeyCombo("ctrl+s");
59
- window.dispatchEvent(new KeyboardEvent("keydown", { key: "s", ctrlKey: true }));
99
+ it("sets pressed=true when matching combo is pressed", () => {
100
+ const kc = new KeyCombo("ctrl+s");
101
+ const { pressed } = kc.setup();
102
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, key: "s" }));
60
103
  expect(pressed()).toBe(true);
61
104
  });
62
- it("resets to false on keyup", () => {
63
- const pressed = useKeyCombo("ctrl+s");
64
- window.dispatchEvent(new KeyboardEvent("keydown", { key: "s", ctrlKey: true }));
105
+ it("resets pressed on keyup", () => {
106
+ const kc = new KeyCombo("ctrl+s");
107
+ const { pressed } = kc.setup();
108
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, key: "s" }));
65
109
  window.dispatchEvent(new KeyboardEvent("keyup"));
66
110
  expect(pressed()).toBe(false);
67
111
  });
112
+ it("does not set pressed for wrong key", () => {
113
+ const kc = new KeyCombo("ctrl+s");
114
+ const { pressed } = kc.setup();
115
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, key: "z" }));
116
+ expect(pressed()).toBe(false);
117
+ });
118
+ it("removes listeners on unmount", () => {
119
+ const remove = vi.spyOn(window, "removeEventListener");
120
+ const kc = new KeyCombo("ctrl+s");
121
+ kc.setup();
122
+ kc.onUnmount();
123
+ expect(remove).toHaveBeenCalledWith("keydown", expect.any(Function));
124
+ expect(remove).toHaveBeenCalledWith("keyup", expect.any(Function));
125
+ });
126
+ it("matches combo without ctrl modifier (ctrl branch: `: true`)", () => {
127
+ const kc = new KeyCombo("s");
128
+ const { pressed } = kc.setup();
129
+ // No ctrlKey needed — parts doesn't include "ctrl" so ctrl = true always
130
+ window.dispatchEvent(new KeyboardEvent("keydown", { key: "s" }));
131
+ expect(pressed()).toBe(true);
132
+ });
133
+ it("matches combo with shift modifier (shift branch: `e.shiftKey`)", () => {
134
+ const kc = new KeyCombo("ctrl+shift+s");
135
+ const { pressed } = kc.setup();
136
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, shiftKey: true, key: "s" }));
137
+ expect(pressed()).toBe(true);
138
+ });
139
+ it("does not match when shift is required but not pressed", () => {
140
+ const kc = new KeyCombo("ctrl+shift+s");
141
+ const { pressed } = kc.setup();
142
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, shiftKey: false, key: "s" }));
143
+ expect(pressed()).toBe(false);
144
+ });
145
+ it("matches combo with alt modifier (alt branch: `e.altKey`)", () => {
146
+ const kc = new KeyCombo("ctrl+alt+s");
147
+ const { pressed } = kc.setup();
148
+ window.dispatchEvent(new KeyboardEvent("keydown", { ctrlKey: true, altKey: true, key: "s" }));
149
+ expect(pressed()).toBe(true);
150
+ });
68
151
  });
69
- // ---------- useIdle ----------
70
- describe("useIdle", () => {
152
+ // ── Idle ──────────────────────────────────────────────────────────────────────
153
+ describe("Idle", () => {
71
154
  it("starts as not idle", () => {
72
155
  vi.useFakeTimers();
73
- const idle = useIdle(500);
74
- expect(idle()).toBe(false);
156
+ const idle = new Idle(500);
157
+ const { idle: isIdle } = idle.setup();
158
+ expect(isIdle()).toBe(false);
75
159
  vi.useRealTimers();
76
160
  });
77
- it("becomes idle after the timeout elapses", () => {
161
+ it("becomes idle after timeout", () => {
78
162
  vi.useFakeTimers();
79
- const idle = useIdle(500);
163
+ const composable = new Idle(500);
164
+ const { idle: isIdle } = composable.setup();
80
165
  vi.advanceTimersByTime(500);
81
- expect(idle()).toBe(true);
166
+ expect(isIdle()).toBe(true);
82
167
  vi.useRealTimers();
83
168
  });
84
- it("resets idle on user activity", () => {
169
+ it("resets idle timer on activity", () => {
85
170
  vi.useFakeTimers();
86
- const idle = useIdle(500);
87
- vi.advanceTimersByTime(300);
171
+ const composable = new Idle(500);
172
+ const { idle: isIdle } = composable.setup();
173
+ vi.advanceTimersByTime(400);
88
174
  window.dispatchEvent(new MouseEvent("mousemove"));
89
- vi.advanceTimersByTime(300); // not enough since activity reset the timer
90
- expect(idle()).toBe(false);
175
+ vi.advanceTimersByTime(400);
176
+ expect(isIdle()).toBe(false);
91
177
  vi.useRealTimers();
92
178
  });
179
+ it("removes listeners on unmount", () => {
180
+ const remove = vi.spyOn(window, "removeEventListener");
181
+ const composable = new Idle(500);
182
+ composable.setup();
183
+ composable.onUnmount();
184
+ expect(remove).toHaveBeenCalled();
185
+ });
93
186
  });
94
187
  //# sourceMappingURL=browser.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../../src/__tests__/browser.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"browser.test.js","sourceRoot":"","sources":["../../src/__tests__/browser.test.ts"],"names":[],"mappings":"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"}
@@ -1,84 +1,211 @@
1
1
  // @vitest-environment jsdom
2
- import { describe, it, expect } from "vitest";
3
- import { createRef, useWindowSize, useScrollPosition, useFocus } from "../dom";
4
- // ---------- createRef ----------
5
- describe("createRef", () => {
6
- it("starts with current = null", () => {
7
- const ref = createRef();
8
- expect(ref.current).toBeNull();
9
- });
10
- it("can be assigned an element", () => {
11
- const ref = createRef();
12
- const el = document.createElement("div");
13
- ref.current = el;
14
- expect(ref.current).toBe(el);
15
- });
16
- });
17
- // ---------- useWindowSize ----------
18
- describe("useWindowSize", () => {
19
- it("returns the current window dimensions", () => {
20
- Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1024 });
21
- Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 768 });
22
- const { width, height } = useWindowSize();
2
+ import { describe, it, expect, vi, beforeEach } from "vitest";
3
+ import { signal } from "@praxisjs/core/internal";
4
+ import { WindowSize, ScrollPosition, ElementSize, Intersection, Focus } from "../dom";
5
+ // ── WindowSize ────────────────────────────────────────────────────────────────
6
+ describe("WindowSize", () => {
7
+ it("reads initial window dimensions", () => {
8
+ Object.defineProperty(window, "innerWidth", { value: 1024, configurable: true });
9
+ Object.defineProperty(window, "innerHeight", { value: 768, configurable: true });
10
+ const ws = new WindowSize();
11
+ const { width, height } = ws.setup();
23
12
  expect(width()).toBe(1024);
24
13
  expect(height()).toBe(768);
25
14
  });
26
15
  it("updates on resize event", () => {
27
- Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 800 });
28
- Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 600 });
29
- const { width, height } = useWindowSize();
30
- Object.defineProperty(window, "innerWidth", { writable: true, configurable: true, value: 1280 });
31
- Object.defineProperty(window, "innerHeight", { writable: true, configurable: true, value: 720 });
16
+ Object.defineProperty(window, "innerWidth", { value: 800, configurable: true });
17
+ Object.defineProperty(window, "innerHeight", { value: 600, configurable: true });
18
+ const ws = new WindowSize();
19
+ const { width, height } = ws.setup();
20
+ Object.defineProperty(window, "innerWidth", { value: 1280, configurable: true });
21
+ Object.defineProperty(window, "innerHeight", { value: 900, configurable: true });
32
22
  window.dispatchEvent(new Event("resize"));
33
23
  expect(width()).toBe(1280);
34
- expect(height()).toBe(720);
24
+ expect(height()).toBe(900);
25
+ });
26
+ it("removes listener on unmount", () => {
27
+ const remove = vi.spyOn(window, "removeEventListener");
28
+ const ws = new WindowSize();
29
+ ws.setup();
30
+ ws.onUnmount();
31
+ expect(remove).toHaveBeenCalledWith("resize", expect.any(Function));
35
32
  });
36
33
  });
37
- // ---------- useScrollPosition ----------
38
- describe("useScrollPosition", () => {
34
+ // ── ScrollPosition ────────────────────────────────────────────────────────────
35
+ describe("ScrollPosition", () => {
39
36
  it("starts at (0, 0)", () => {
40
- const { x, y } = useScrollPosition(window);
37
+ const sp = new ScrollPosition(window);
38
+ const { x, y } = sp.setup();
41
39
  expect(x()).toBe(0);
42
40
  expect(y()).toBe(0);
43
41
  });
44
- it("updates on element scroll", () => {
42
+ it("updates on scroll event for window", () => {
43
+ const sp = new ScrollPosition(window);
44
+ const { x, y } = sp.setup();
45
+ Object.defineProperty(window, "scrollX", { value: 100, configurable: true });
46
+ Object.defineProperty(window, "scrollY", { value: 200, configurable: true });
47
+ window.dispatchEvent(new Event("scroll"));
48
+ expect(x()).toBe(100);
49
+ expect(y()).toBe(200);
50
+ });
51
+ it("updates on scroll event for element", () => {
45
52
  const el = document.createElement("div");
46
- const { x, y } = useScrollPosition(el);
47
- Object.defineProperty(el, "scrollLeft", { configurable: true, value: 50 });
48
- Object.defineProperty(el, "scrollTop", { configurable: true, value: 80 });
53
+ const sp = new ScrollPosition(el);
54
+ const { x, y } = sp.setup();
55
+ Object.defineProperty(el, "scrollLeft", { value: 50, configurable: true });
56
+ Object.defineProperty(el, "scrollTop", { value: 75, configurable: true });
49
57
  el.dispatchEvent(new Event("scroll"));
50
58
  expect(x()).toBe(50);
51
- expect(y()).toBe(80);
59
+ expect(y()).toBe(75);
60
+ });
61
+ it("removes listener on unmount", () => {
62
+ const el = document.createElement("div");
63
+ const remove = vi.spyOn(el, "removeEventListener");
64
+ const sp = new ScrollPosition(el);
65
+ sp.setup();
66
+ sp.onUnmount();
67
+ expect(remove).toHaveBeenCalledWith("scroll", expect.any(Function));
52
68
  });
53
69
  });
54
- // ---------- useFocus ----------
55
- describe("useFocus", () => {
70
+ // ── ElementSize ───────────────────────────────────────────────────────────────
71
+ describe("ElementSize", () => {
72
+ let observerCallback;
73
+ beforeEach(() => {
74
+ vi.stubGlobal("ResizeObserver", class {
75
+ constructor(cb) {
76
+ this.observe = vi.fn();
77
+ this.disconnect = vi.fn();
78
+ observerCallback = cb;
79
+ }
80
+ });
81
+ });
82
+ it("starts at (0, 0) with no element", () => {
83
+ const ref = { current: null };
84
+ const es = new ElementSize(ref);
85
+ const { width, height } = es.setup();
86
+ expect(width()).toBe(0);
87
+ expect(height()).toBe(0);
88
+ });
89
+ it("updates via ResizeObserver callback", () => {
90
+ const el = document.createElement("div");
91
+ const ref = { current: el };
92
+ const es = new ElementSize(ref);
93
+ const { width, height } = es.setup();
94
+ observerCallback([{ contentRect: { width: 300, height: 150 } }], {});
95
+ expect(width()).toBe(300);
96
+ expect(height()).toBe(150);
97
+ });
98
+ it("disconnects observer on unmount", () => {
99
+ const disconnect = vi.fn();
100
+ vi.stubGlobal("ResizeObserver", class {
101
+ constructor(cb) {
102
+ this.observe = vi.fn();
103
+ this.disconnect = disconnect;
104
+ observerCallback = cb;
105
+ }
106
+ });
107
+ const ref = { current: null };
108
+ const es = new ElementSize(ref);
109
+ es.setup();
110
+ es.onUnmount();
111
+ expect(disconnect).toHaveBeenCalled();
112
+ });
113
+ });
114
+ // ── Intersection ──────────────────────────────────────────────────────────────
115
+ describe("Intersection", () => {
116
+ let observerCallback;
117
+ beforeEach(() => {
118
+ vi.stubGlobal("IntersectionObserver", class {
119
+ constructor(cb) {
120
+ this.observe = vi.fn();
121
+ this.disconnect = vi.fn();
122
+ observerCallback = cb;
123
+ }
124
+ });
125
+ });
126
+ it("starts as not visible", () => {
127
+ const ref = { current: null };
128
+ const int = new Intersection(ref);
129
+ const { visible } = int.setup();
130
+ expect(visible()).toBe(false);
131
+ });
132
+ it("becomes visible when intersecting", () => {
133
+ const el = document.createElement("div");
134
+ const ref = { current: el };
135
+ const int = new Intersection(ref);
136
+ const { visible } = int.setup();
137
+ observerCallback([{ isIntersecting: true }], {});
138
+ expect(visible()).toBe(true);
139
+ });
140
+ it("disconnects on unmount", () => {
141
+ const disconnect = vi.fn();
142
+ vi.stubGlobal("IntersectionObserver", class {
143
+ constructor(cb) {
144
+ this.observe = vi.fn();
145
+ this.disconnect = disconnect;
146
+ observerCallback = cb;
147
+ }
148
+ });
149
+ const ref = { current: null };
150
+ const int = new Intersection(ref);
151
+ int.setup();
152
+ int.onUnmount();
153
+ expect(disconnect).toHaveBeenCalled();
154
+ });
155
+ });
156
+ // ── Focus ─────────────────────────────────────────────────────────────────────
157
+ describe("Focus", () => {
56
158
  it("starts as not focused", () => {
57
159
  const el = document.createElement("input");
58
- document.body.appendChild(el);
59
160
  const ref = { current: el };
60
- const focused = useFocus(ref);
161
+ const focus = new Focus(ref);
162
+ const { focused } = focus.setup();
61
163
  expect(focused()).toBe(false);
62
- document.body.removeChild(el);
63
164
  });
64
- it("becomes true on focus event", () => {
165
+ it("sets focused=true on focus event", () => {
65
166
  const el = document.createElement("input");
66
167
  document.body.appendChild(el);
67
168
  const ref = { current: el };
68
- const focused = useFocus(ref);
69
- el.dispatchEvent(new FocusEvent("focus"));
169
+ const focus = new Focus(ref);
170
+ const { focused } = focus.setup();
171
+ el.dispatchEvent(new Event("focus"));
70
172
  expect(focused()).toBe(true);
71
173
  document.body.removeChild(el);
72
174
  });
73
- it("becomes false on blur event", () => {
175
+ it("sets focused=false on blur event", () => {
74
176
  const el = document.createElement("input");
75
177
  document.body.appendChild(el);
76
178
  const ref = { current: el };
77
- const focused = useFocus(ref);
78
- el.dispatchEvent(new FocusEvent("focus"));
79
- el.dispatchEvent(new FocusEvent("blur"));
179
+ const focus = new Focus(ref);
180
+ const { focused } = focus.setup();
181
+ el.dispatchEvent(new Event("focus"));
182
+ el.dispatchEvent(new Event("blur"));
80
183
  expect(focused()).toBe(false);
81
184
  document.body.removeChild(el);
82
185
  });
186
+ it("does not throw when ref is null", () => {
187
+ const ref = { current: null };
188
+ const focus = new Focus(ref);
189
+ expect(() => focus.setup()).not.toThrow();
190
+ });
191
+ it("removes focus/blur listeners when reactive ref changes (effect cleanup)", () => {
192
+ const el1 = document.createElement("input");
193
+ document.body.appendChild(el1);
194
+ // Make ref.current reactive so the effect tracks it and re-runs on change
195
+ const refSignal = signal(el1);
196
+ const ref = { get current() { return refSignal(); } };
197
+ const focus = new Focus(ref);
198
+ focus.setup();
199
+ const removeEl1 = vi.spyOn(el1, "removeEventListener");
200
+ // Changing the signal causes the effect to re-run, calling cleanup for el1
201
+ // This covers the cleanup function at dom.ts:134-135
202
+ const el2 = document.createElement("input");
203
+ document.body.appendChild(el2);
204
+ refSignal.set(el2);
205
+ expect(removeEl1).toHaveBeenCalledWith("focus", expect.any(Function));
206
+ expect(removeEl1).toHaveBeenCalledWith("blur", expect.any(Function));
207
+ document.body.removeChild(el1);
208
+ document.body.removeChild(el2);
209
+ });
83
210
  });
84
211
  //# sourceMappingURL=dom.test.js.map