@sveltia/ui 0.36.1 → 0.37.1
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/dist/components/util/popup.svelte +16 -29
- package/dist/services/popup.svelte.d.ts +11 -12
- package/dist/services/popup.svelte.js +51 -44
- package/package.json +61 -53
- package/dist/components/text-editor/constants.test.d.ts +0 -1
- package/dist/components/text-editor/constants.test.js +0 -98
- package/dist/components/text-editor/markdown.test.d.ts +0 -1
- package/dist/components/text-editor/markdown.test.js +0 -84
- package/dist/components/text-editor/store.svelte.test.d.ts +0 -1
- package/dist/components/text-editor/store.svelte.test.js +0 -229
- package/dist/components/text-editor/transformers/hr.test.d.ts +0 -1
- package/dist/components/text-editor/transformers/hr.test.js +0 -106
- package/dist/components/text-editor/transformers/table.test.d.ts +0 -1
- package/dist/components/text-editor/transformers/table.test.js +0 -28
- package/dist/services/group.test.d.ts +0 -1
- package/dist/services/group.test.js +0 -1215
- package/dist/services/i18n.test.d.ts +0 -1
- package/dist/services/i18n.test.js +0 -18
- package/dist/services/popup.test.d.ts +0 -1
- package/dist/services/popup.test.js +0 -641
- package/dist/services/select.test.d.ts +0 -1
- package/dist/services/select.test.js +0 -69
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { initLocales } from './i18n.js';
|
|
3
|
-
|
|
4
|
-
describe('initLocales', () => {
|
|
5
|
-
it('should not throw when called with default options', () => {
|
|
6
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
7
|
-
|
|
8
|
-
expect(() => initLocales()).not.toThrow();
|
|
9
|
-
warnSpy.mockRestore();
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should not throw when called with custom locale options', () => {
|
|
13
|
-
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
14
|
-
|
|
15
|
-
expect(() => initLocales({ fallbackLocale: 'en', initialLocale: 'ja' })).not.toThrow();
|
|
16
|
-
warnSpy.mockRestore();
|
|
17
|
-
});
|
|
18
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
/* eslint-disable jsdoc/require-description */
|
|
2
|
-
/* eslint-disable jsdoc/require-jsdoc */
|
|
3
|
-
/* eslint-disable jsdoc/require-param */
|
|
4
|
-
/* eslint-disable jsdoc/require-param-description */
|
|
5
|
-
/* eslint-disable jsdoc/require-returns */
|
|
6
|
-
/* eslint-disable lines-between-class-members */
|
|
7
|
-
/* eslint-disable max-classes-per-file */
|
|
8
|
-
|
|
9
|
-
import { get } from 'svelte/store';
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import { activatePopup } from './popup.svelte.js';
|
|
12
|
-
|
|
13
|
-
describe('Popup', () => {
|
|
14
|
-
/** @type {HTMLButtonElement} */
|
|
15
|
-
let anchor;
|
|
16
|
-
/** @type {HTMLDialogElement} */
|
|
17
|
-
let popup;
|
|
18
|
-
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
21
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
22
|
-
document.body.appendChild(anchor);
|
|
23
|
-
document.body.appendChild(popup);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
anchor.remove();
|
|
28
|
-
popup.remove();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should assign an id to the popup element', () => {
|
|
32
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
33
|
-
expect(popup.id).toBeTruthy();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should set aria-controls on the anchor to match the popup id', () => {
|
|
37
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
38
|
-
expect(anchor.getAttribute('aria-controls')).toBe(popup.id);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should expose the open store defaulting to false', () => {
|
|
42
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
43
|
-
|
|
44
|
-
expect(get(instance.open)).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should set aria-expanded to false initially', () => {
|
|
48
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
49
|
-
expect(anchor.getAttribute('aria-expanded')).toBe('false');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should toggle open store to true on anchor click', () => {
|
|
53
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
54
|
-
|
|
55
|
-
anchor.click();
|
|
56
|
-
expect(get(instance.open)).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should set aria-expanded to true after anchor click', () => {
|
|
60
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
61
|
-
anchor.click();
|
|
62
|
-
expect(anchor.getAttribute('aria-expanded')).toBe('true');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('should not toggle open when anchor is disabled', () => {
|
|
66
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
67
|
-
|
|
68
|
-
anchor.setAttribute('aria-disabled', 'true');
|
|
69
|
-
anchor.click();
|
|
70
|
-
expect(get(instance.open)).toBe(false);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should not toggle open when anchor is read-only', () => {
|
|
74
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
75
|
-
|
|
76
|
-
anchor.setAttribute('aria-readonly', 'true');
|
|
77
|
-
anchor.click();
|
|
78
|
-
expect(get(instance.open)).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should report isDisabled as false when not disabled', () => {
|
|
82
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
83
|
-
|
|
84
|
-
expect(instance.isDisabled).toBe(false);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should report isDisabled as true when aria-disabled is set', () => {
|
|
88
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
89
|
-
|
|
90
|
-
anchor.setAttribute('aria-disabled', 'true');
|
|
91
|
-
expect(instance.isDisabled).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should report isReadOnly as false when not read-only', () => {
|
|
95
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
96
|
-
|
|
97
|
-
expect(instance.isReadOnly).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should report isReadOnly as true when aria-readonly is set', () => {
|
|
101
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
102
|
-
|
|
103
|
-
anchor.setAttribute('aria-readonly', 'true');
|
|
104
|
-
expect(instance.isReadOnly).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should close on Escape keydown on popup', () => {
|
|
108
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
109
|
-
|
|
110
|
-
anchor.click(); // open first
|
|
111
|
-
popup.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: false }));
|
|
112
|
-
expect(get(instance.open)).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should close when a menu option inside popup is clicked', () => {
|
|
116
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
117
|
-
const menuItem = document.createElement('div');
|
|
118
|
-
|
|
119
|
-
menuItem.setAttribute('role', 'menuitem');
|
|
120
|
-
popup.appendChild(menuItem);
|
|
121
|
-
anchor.click(); // open
|
|
122
|
-
menuItem.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
123
|
-
expect(get(instance.open)).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should not close popup when clicking a non-menuitem element inside it (branch 35 false)', () => {
|
|
127
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
128
|
-
const div = document.createElement('div');
|
|
129
|
-
|
|
130
|
-
popup.appendChild(div);
|
|
131
|
-
anchor.click(); // open
|
|
132
|
-
div.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
133
|
-
// div has no role → neither menuitem nor popup backdrop → popup stays open
|
|
134
|
-
expect(get(instance.open)).toBe(true);
|
|
135
|
-
div.remove();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should not close popup on Escape with modifier key held (branch 38 false)', () => {
|
|
139
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
140
|
-
|
|
141
|
-
anchor.click(); // open
|
|
142
|
-
popup.dispatchEvent(
|
|
143
|
-
new KeyboardEvent('keydown', { key: 'Escape', shiftKey: true, bubbles: false }),
|
|
144
|
-
);
|
|
145
|
-
// hasModifier=true → condition false → popup stays open
|
|
146
|
-
expect(get(instance.open)).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should toggle open to true on Enter keydown on anchor', () => {
|
|
150
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
151
|
-
|
|
152
|
-
anchor.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
153
|
-
expect(get(instance.open)).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should toggle open to true on Space keydown on anchor', () => {
|
|
157
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
158
|
-
|
|
159
|
-
anchor.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
|
|
160
|
-
expect(get(instance.open)).toBe(true);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('should not toggle open when anchor keydown has a modifier key', () => {
|
|
164
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
165
|
-
|
|
166
|
-
anchor.dispatchEvent(
|
|
167
|
-
new KeyboardEvent('keydown', { key: 'Enter', ctrlKey: true, bubbles: true }),
|
|
168
|
-
);
|
|
169
|
-
expect(get(instance.open)).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('should close on click directly on the popup backdrop element', () => {
|
|
173
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
174
|
-
|
|
175
|
-
anchor.click(); // open
|
|
176
|
-
popup.dispatchEvent(new MouseEvent('click', { bubbles: false }));
|
|
177
|
-
expect(get(instance.open)).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should remove aria-controls from anchor when popup closes after being open', () => {
|
|
181
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
182
|
-
anchor.click(); // open → aria-expanded='true'
|
|
183
|
-
anchor.click(); // close → aria-controls should be removed
|
|
184
|
-
expect(anchor.getAttribute('aria-controls')).toBeNull();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should set aria-expanded to false after closing', () => {
|
|
188
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
189
|
-
anchor.click(); // open
|
|
190
|
-
anchor.click(); // close
|
|
191
|
-
expect(anchor.getAttribute('aria-expanded')).toBe('false');
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe('Popup - hideImmediately', () => {
|
|
196
|
-
/** @type {HTMLButtonElement} */
|
|
197
|
-
let anchor;
|
|
198
|
-
/** @type {HTMLDialogElement} */
|
|
199
|
-
let popup;
|
|
200
|
-
|
|
201
|
-
beforeEach(() => {
|
|
202
|
-
vi.useFakeTimers();
|
|
203
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
204
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
205
|
-
document.body.appendChild(anchor);
|
|
206
|
-
document.body.appendChild(popup);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
afterEach(() => {
|
|
210
|
-
anchor.remove();
|
|
211
|
-
popup.remove();
|
|
212
|
-
vi.useRealTimers();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('should set open to false immediately when hideImmediately is called', async () => {
|
|
216
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
217
|
-
|
|
218
|
-
anchor.click();
|
|
219
|
-
expect(get(instance.open)).toBe(true);
|
|
220
|
-
|
|
221
|
-
const hidePromise = instance.hideImmediately();
|
|
222
|
-
|
|
223
|
-
expect(get(instance.open)).toBe(false);
|
|
224
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
225
|
-
await hidePromise;
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('should temporarily set popup.hidden and then restore it', async () => {
|
|
229
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
230
|
-
|
|
231
|
-
anchor.click();
|
|
232
|
-
|
|
233
|
-
const hidePromise = instance.hideImmediately();
|
|
234
|
-
|
|
235
|
-
expect(popup.hidden).toBe(true);
|
|
236
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
237
|
-
await hidePromise;
|
|
238
|
-
expect(popup.hidden).toBe(false);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
describe('Popup - transitionstart', () => {
|
|
243
|
-
/** @type {HTMLButtonElement} */
|
|
244
|
-
let anchor;
|
|
245
|
-
/** @type {HTMLDialogElement} */
|
|
246
|
-
let popup;
|
|
247
|
-
/** @type {HTMLDivElement} */
|
|
248
|
-
let wrapper;
|
|
249
|
-
|
|
250
|
-
beforeEach(() => {
|
|
251
|
-
vi.useFakeTimers();
|
|
252
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
253
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
254
|
-
wrapper = document.createElement('div');
|
|
255
|
-
wrapper.appendChild(anchor);
|
|
256
|
-
document.body.appendChild(wrapper);
|
|
257
|
-
document.body.appendChild(popup);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
afterEach(() => {
|
|
261
|
-
wrapper.remove();
|
|
262
|
-
popup.remove();
|
|
263
|
-
vi.useRealTimers();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it('should hide popup when anchor transitions inside a .hiding ancestor', async () => {
|
|
267
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
268
|
-
|
|
269
|
-
anchor.click();
|
|
270
|
-
expect(get(instance.open)).toBe(true);
|
|
271
|
-
wrapper.classList.add('hiding');
|
|
272
|
-
anchor.dispatchEvent(new Event('transitionstart'));
|
|
273
|
-
expect(get(instance.open)).toBe(false);
|
|
274
|
-
wrapper.classList.remove('hiding');
|
|
275
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should not hide popup on transitionstart when no hiding ancestor', async () => {
|
|
279
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
280
|
-
|
|
281
|
-
anchor.click();
|
|
282
|
-
anchor.dispatchEvent(new Event('transitionstart'));
|
|
283
|
-
expect(get(instance.open)).toBe(true);
|
|
284
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
describe('Popup - IntersectionObserver on anchor (lines 179-180)', () => {
|
|
289
|
-
/** @type {HTMLButtonElement} */
|
|
290
|
-
let anchor;
|
|
291
|
-
/** @type {HTMLDialogElement} */
|
|
292
|
-
let popup;
|
|
293
|
-
/** @type {typeof globalThis.IntersectionObserver} */
|
|
294
|
-
let OrigIObserver;
|
|
295
|
-
/** @type {((entries: any[]) => void)[]} */
|
|
296
|
-
let ioCallbacks;
|
|
297
|
-
|
|
298
|
-
beforeEach(() => {
|
|
299
|
-
vi.useFakeTimers();
|
|
300
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
301
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
302
|
-
document.body.appendChild(anchor);
|
|
303
|
-
document.body.appendChild(popup);
|
|
304
|
-
ioCallbacks = [];
|
|
305
|
-
OrigIObserver = globalThis.IntersectionObserver;
|
|
306
|
-
|
|
307
|
-
// Stub IntersectionObserver to capture callbacks
|
|
308
|
-
globalThis.IntersectionObserver = /** @type {any} */ (
|
|
309
|
-
class {
|
|
310
|
-
/** @param {(entries: any[]) => void} cb */
|
|
311
|
-
constructor(cb) {
|
|
312
|
-
ioCallbacks.push(cb);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
observe() {}
|
|
316
|
-
unobserve() {}
|
|
317
|
-
disconnect() {}
|
|
318
|
-
}
|
|
319
|
-
);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
afterEach(async () => {
|
|
323
|
-
anchor.remove();
|
|
324
|
-
popup.remove();
|
|
325
|
-
globalThis.IntersectionObserver = OrigIObserver;
|
|
326
|
-
await vi.runAllTimersAsync();
|
|
327
|
-
vi.useRealTimers();
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('should hide when anchor becomes non-intersecting while popup is open', async () => {
|
|
331
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
332
|
-
|
|
333
|
-
anchor.click();
|
|
334
|
-
expect(get(instance.open)).toBe(true);
|
|
335
|
-
|
|
336
|
-
// ioCallbacks[0] = position observer; ioCallbacks[1] = anchor visibility observer
|
|
337
|
-
const anchorVisibilityCallback = ioCallbacks[1];
|
|
338
|
-
|
|
339
|
-
anchorVisibilityCallback([{ isIntersecting: false }]);
|
|
340
|
-
expect(get(instance.open)).toBe(false);
|
|
341
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
it('should not hide when anchor leaves viewport but popup is already closed', () => {
|
|
345
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
346
|
-
// popup is not opened
|
|
347
|
-
const anchorVisibilityCallback = ioCallbacks[1];
|
|
348
|
-
|
|
349
|
-
anchorVisibilityCallback([{ isIntersecting: false }]);
|
|
350
|
-
expect(get(instance.open)).toBe(false);
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
describe('Popup - IntersectionObserver position callback (lines 35-132)', () => {
|
|
354
|
-
/** @type {HTMLButtonElement} */
|
|
355
|
-
let anchor;
|
|
356
|
-
/** @type {HTMLDialogElement} */
|
|
357
|
-
let popup;
|
|
358
|
-
/** @type {HTMLDivElement} */
|
|
359
|
-
let content;
|
|
360
|
-
/** @type {typeof globalThis.IntersectionObserver} */
|
|
361
|
-
let OrigIObserver;
|
|
362
|
-
/** @type {((entries: any[]) => void)[]} */
|
|
363
|
-
let ioCallbacks;
|
|
364
|
-
|
|
365
|
-
beforeEach(() => {
|
|
366
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
367
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
368
|
-
// The position callback accesses popup.querySelector('.content')
|
|
369
|
-
content = document.createElement('div');
|
|
370
|
-
content.className = 'content';
|
|
371
|
-
popup.appendChild(content);
|
|
372
|
-
document.body.appendChild(anchor);
|
|
373
|
-
document.body.appendChild(popup);
|
|
374
|
-
|
|
375
|
-
ioCallbacks = [];
|
|
376
|
-
OrigIObserver = globalThis.IntersectionObserver;
|
|
377
|
-
globalThis.IntersectionObserver = /** @type {any} */ (
|
|
378
|
-
class {
|
|
379
|
-
/** @param {(entries: any[]) => void} cb */
|
|
380
|
-
constructor(cb) {
|
|
381
|
-
ioCallbacks.push(cb);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
observe() {}
|
|
385
|
-
unobserve() {}
|
|
386
|
-
disconnect() {}
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
afterEach(() => {
|
|
392
|
-
anchor.remove();
|
|
393
|
-
popup.remove();
|
|
394
|
-
globalThis.IntersectionObserver = OrigIObserver;
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Helper to create a fake intersection entry.
|
|
399
|
-
*/
|
|
400
|
-
const makeEntry = ({
|
|
401
|
-
top = 100,
|
|
402
|
-
bottom = 150,
|
|
403
|
-
left = 50,
|
|
404
|
-
right = 300,
|
|
405
|
-
vw = 800,
|
|
406
|
-
vh = 600,
|
|
407
|
-
} = {}) => ({
|
|
408
|
-
intersectionRect: { top, bottom, left, right, width: right - left, height: bottom - top },
|
|
409
|
-
rootBounds: { width: vw, height: vh },
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('should set popup style on intersection (bottom-left, normal case)', () => {
|
|
413
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
414
|
-
|
|
415
|
-
// ioCallbacks[0] is the position observer
|
|
416
|
-
ioCallbacks[0]([makeEntry()]);
|
|
417
|
-
|
|
418
|
-
// Style should be updated with computed inset
|
|
419
|
-
const style = get(instance.style);
|
|
420
|
-
|
|
421
|
-
expect(style.inset).toBeTruthy();
|
|
422
|
-
expect(style.zIndex).toBe(1000);
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
it('should skip entry when intersectionRect is null', () => {
|
|
426
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
427
|
-
|
|
428
|
-
ioCallbacks[0]([{ intersectionRect: null, rootBounds: null }]);
|
|
429
|
-
|
|
430
|
-
// Style remains at default (no crash)
|
|
431
|
-
const style = get(instance.style);
|
|
432
|
-
|
|
433
|
-
expect(style.inset).toBeUndefined();
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it('should switch position to top-left when content overflows bottom', () => {
|
|
437
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
438
|
-
|
|
439
|
-
// contentHeight > bottomMargin AND topMargin > bottomMargin → switches to top-
|
|
440
|
-
Object.defineProperty(content, 'scrollHeight', { configurable: true, get: () => 500 });
|
|
441
|
-
ioCallbacks[0]([makeEntry({ top: 400, bottom: 450, left: 50, right: 300, vw: 800, vh: 500 })]);
|
|
442
|
-
|
|
443
|
-
const style = get(instance.style);
|
|
444
|
-
|
|
445
|
-
// Position changed to top-left → bottom should be calculated (not auto)
|
|
446
|
-
expect(style.inset).not.toBeUndefined();
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it('should switch position to bottom-right when content overflows to the right', () => {
|
|
450
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
451
|
-
|
|
452
|
-
// contentWidth > remaining right space → switch to bottom-right
|
|
453
|
-
Object.defineProperty(content, 'scrollWidth', { configurable: true, get: () => 760 });
|
|
454
|
-
ioCallbacks[0]([makeEntry({ left: 50, right: 300 })]);
|
|
455
|
-
|
|
456
|
-
const style = get(instance.style);
|
|
457
|
-
|
|
458
|
-
expect(style.inset).not.toBeUndefined();
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it('should switch position to bottom-left when content overflows to the left', () => {
|
|
462
|
-
const instance = activatePopup(anchor, popup, 'bottom-right');
|
|
463
|
-
|
|
464
|
-
// contentWidth causes left edge to be < 8 → switch to bottom-left
|
|
465
|
-
Object.defineProperty(content, 'scrollWidth', { configurable: true, get: () => 290 });
|
|
466
|
-
ioCallbacks[0]([makeEntry({ left: 50, right: 100 })]);
|
|
467
|
-
|
|
468
|
-
const style = get(instance.style);
|
|
469
|
-
|
|
470
|
-
expect(style.inset).not.toBeUndefined();
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it('should normalize RTL position when document.dir is rtl', () => {
|
|
474
|
-
Object.defineProperty(document, 'dir', { get: () => 'rtl', configurable: true });
|
|
475
|
-
|
|
476
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
477
|
-
|
|
478
|
-
ioCallbacks[0]([makeEntry()]);
|
|
479
|
-
|
|
480
|
-
const style = get(instance.style);
|
|
481
|
-
|
|
482
|
-
expect(style.inset).not.toBeUndefined();
|
|
483
|
-
Reflect.deleteProperty(document, 'dir');
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('should normalize bottom-right to bottom-left in RTL (endsWith -right branch)', () => {
|
|
487
|
-
Object.defineProperty(document, 'dir', { get: () => 'rtl', configurable: true });
|
|
488
|
-
|
|
489
|
-
const instance = activatePopup(anchor, popup, 'bottom-right');
|
|
490
|
-
|
|
491
|
-
ioCallbacks[0]([makeEntry()]);
|
|
492
|
-
|
|
493
|
-
const style = get(instance.style);
|
|
494
|
-
|
|
495
|
-
// After RTL normalization bottom-right → bottom-left; inset should be computed
|
|
496
|
-
expect(style.inset).not.toBeUndefined();
|
|
497
|
-
Reflect.deleteProperty(document, 'dir');
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('should normalize left-top to right-top in RTL (startsWith left- branch)', () => {
|
|
501
|
-
Object.defineProperty(document, 'dir', { get: () => 'rtl', configurable: true });
|
|
502
|
-
|
|
503
|
-
const instance = activatePopup(anchor, popup, 'left-top');
|
|
504
|
-
|
|
505
|
-
ioCallbacks[0]([makeEntry()]);
|
|
506
|
-
|
|
507
|
-
const style = get(instance.style);
|
|
508
|
-
|
|
509
|
-
// After RTL normalization left-top → right-top; inset should be computed
|
|
510
|
-
expect(style.inset).not.toBeUndefined();
|
|
511
|
-
Reflect.deleteProperty(document, 'dir');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('should normalize right-top to left-top in RTL (startsWith right- branch)', () => {
|
|
515
|
-
Object.defineProperty(document, 'dir', { get: () => 'rtl', configurable: true });
|
|
516
|
-
|
|
517
|
-
const instance = activatePopup(anchor, popup, 'right-top');
|
|
518
|
-
|
|
519
|
-
ioCallbacks[0]([makeEntry()]);
|
|
520
|
-
|
|
521
|
-
const style = get(instance.style);
|
|
522
|
-
|
|
523
|
-
// After RTL normalization right-top → left-top; inset should be computed
|
|
524
|
-
expect(style.inset).not.toBeUndefined();
|
|
525
|
-
Reflect.deleteProperty(document, 'dir');
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
it('should set height to bottomMargin when content overflows bottom but top is not better', () => {
|
|
529
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
530
|
-
|
|
531
|
-
// bottomMargin = 500 - 400 - 8 = 92; topMargin = 50 - 8 = 42; topMargin < bottomMargin
|
|
532
|
-
// so the else branch runs: height = bottomMargin (92px)
|
|
533
|
-
Object.defineProperty(content, 'scrollHeight', { configurable: true, get: () => 200 });
|
|
534
|
-
ioCallbacks[0]([makeEntry({ top: 50, bottom: 400, vw: 800, vh: 500 })]);
|
|
535
|
-
|
|
536
|
-
const style = get(instance.style);
|
|
537
|
-
|
|
538
|
-
expect(style.height).toBe('92px');
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
it('should compute bottom from rootBounds.height - intersectionRect.bottom for -bottom position (branch 19)', () => {
|
|
542
|
-
// 'right-bottom' ends with '-bottom' → bottom = Math.round(vh - intersectionRect.bottom)
|
|
543
|
-
const instance = activatePopup(anchor, popup, 'right-bottom');
|
|
544
|
-
|
|
545
|
-
// default: top=100, bottom=150, left=50, right=300, vh=600
|
|
546
|
-
// bottom = Math.round(600 - 150) = 450
|
|
547
|
-
ioCallbacks[0]([makeEntry()]);
|
|
548
|
-
|
|
549
|
-
const style = get(instance.style);
|
|
550
|
-
|
|
551
|
-
expect(style.inset).toContain('450px');
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it('should not update style when intersection callback fires with identical geometry (branch 25)', () => {
|
|
555
|
-
const instance = activatePopup(anchor, popup, 'bottom-left');
|
|
556
|
-
const entry = makeEntry();
|
|
557
|
-
|
|
558
|
-
// First call — style is updated (inset differs from initial empty object)
|
|
559
|
-
ioCallbacks[0]([entry]);
|
|
560
|
-
|
|
561
|
-
const styleBefore = get(instance.style);
|
|
562
|
-
|
|
563
|
-
// Second call with same entry — all comparisons are equal → style.set not called again
|
|
564
|
-
ioCallbacks[0]([entry]);
|
|
565
|
-
|
|
566
|
-
const styleAfter = get(instance.style);
|
|
567
|
-
|
|
568
|
-
expect(styleAfter.inset).toBe(styleBefore.inset);
|
|
569
|
-
expect(styleAfter.zIndex).toBe(styleBefore.zIndex);
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
describe('Popup - ResizeObserver callback (lines 223-224)', () => {
|
|
573
|
-
/** @type {HTMLButtonElement} */
|
|
574
|
-
let anchor;
|
|
575
|
-
/** @type {HTMLDialogElement} */
|
|
576
|
-
let popup;
|
|
577
|
-
/** @type {typeof globalThis.ResizeObserver} */
|
|
578
|
-
let OrigRObs;
|
|
579
|
-
/** @type {((entries: any[]) => void) | undefined} */
|
|
580
|
-
let resizeCallback;
|
|
581
|
-
/** @type {typeof globalThis.IntersectionObserver | undefined} */
|
|
582
|
-
let _OrigIO;
|
|
583
|
-
|
|
584
|
-
beforeEach(() => {
|
|
585
|
-
vi.useFakeTimers();
|
|
586
|
-
anchor = /** @type {HTMLButtonElement} */ (document.createElement('button'));
|
|
587
|
-
popup = /** @type {HTMLDialogElement} */ (document.createElement('dialog'));
|
|
588
|
-
document.body.appendChild(anchor);
|
|
589
|
-
document.body.appendChild(popup);
|
|
590
|
-
OrigRObs = globalThis.ResizeObserver;
|
|
591
|
-
|
|
592
|
-
// Stub ResizeObserver to capture its callback
|
|
593
|
-
globalThis.ResizeObserver = /** @type {any} */ (
|
|
594
|
-
class {
|
|
595
|
-
/** @param {any} cb */
|
|
596
|
-
constructor(cb) {
|
|
597
|
-
resizeCallback = cb;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
observe() {}
|
|
601
|
-
unobserve() {}
|
|
602
|
-
disconnect() {}
|
|
603
|
-
}
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
// Also stub IntersectionObserver to avoid errors
|
|
607
|
-
const OrigIO = globalThis.IntersectionObserver;
|
|
608
|
-
|
|
609
|
-
globalThis.IntersectionObserver = /** @type {any} */ (
|
|
610
|
-
class {
|
|
611
|
-
// eslint-disable-next-line no-useless-constructor, no-empty-function
|
|
612
|
-
constructor() {}
|
|
613
|
-
observe() {}
|
|
614
|
-
unobserve() {}
|
|
615
|
-
disconnect() {}
|
|
616
|
-
}
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
_OrigIO = OrigIO;
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
afterEach(async () => {
|
|
623
|
-
anchor.remove();
|
|
624
|
-
popup.remove();
|
|
625
|
-
globalThis.ResizeObserver = OrigRObs;
|
|
626
|
-
globalThis.IntersectionObserver = /** @type {any} */ (_OrigIO);
|
|
627
|
-
await vi.runAllTimersAsync();
|
|
628
|
-
vi.useRealTimers();
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it('should schedule checkPosition via RAF when resize is observed', async () => {
|
|
632
|
-
activatePopup(anchor, popup, 'bottom-left');
|
|
633
|
-
|
|
634
|
-
// Trigger the ResizeObserver callback (lines 223-224: cancelAnimationFrame +
|
|
635
|
-
// requestAnimationFrame)
|
|
636
|
-
/** @type {any} */ (resizeCallback)?.([]);
|
|
637
|
-
await vi.advanceTimersByTimeAsync(16); // flush rAF
|
|
638
|
-
// No crash; just verifies these lines execute
|
|
639
|
-
expect(true).toBe(true);
|
|
640
|
-
});
|
|
641
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|