@reshaped/utilities 3.9.1-canary.2

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 (58) hide show
  1. package/LICENSE.md +21 -0
  2. package/dist/dom/getShadowRoot.d.ts +2 -0
  3. package/dist/dom/getShadowRoot.js +5 -0
  4. package/dist/flyout/Flyout.d.ts +13 -0
  5. package/dist/flyout/Flyout.js +101 -0
  6. package/dist/flyout/constants.d.ts +9 -0
  7. package/dist/flyout/constants.js +9 -0
  8. package/dist/flyout/index.d.ts +1 -0
  9. package/dist/flyout/index.js +1 -0
  10. package/dist/flyout/tests/Flyout.test.d.ts +1 -0
  11. package/dist/flyout/tests/Flyout.test.js +129 -0
  12. package/dist/flyout/types.d.ts +24 -0
  13. package/dist/flyout/types.js +1 -0
  14. package/dist/flyout/utilities/applyPosition.d.ts +7 -0
  15. package/dist/flyout/utilities/applyPosition.js +103 -0
  16. package/dist/flyout/utilities/calculatePosition.d.ts +33 -0
  17. package/dist/flyout/utilities/calculatePosition.js +159 -0
  18. package/dist/flyout/utilities/centerBySize.d.ts +5 -0
  19. package/dist/flyout/utilities/centerBySize.js +7 -0
  20. package/dist/flyout/utilities/findClosestFixedContainer.d.ts +5 -0
  21. package/dist/flyout/utilities/findClosestFixedContainer.js +18 -0
  22. package/dist/flyout/utilities/findClosestScrollableContainer.d.ts +5 -0
  23. package/dist/flyout/utilities/findClosestScrollableContainer.js +12 -0
  24. package/dist/flyout/utilities/getPositionFallbacks.d.ts +8 -0
  25. package/dist/flyout/utilities/getPositionFallbacks.js +43 -0
  26. package/dist/flyout/utilities/getRTLPosition.d.ts +3 -0
  27. package/dist/flyout/utilities/getRTLPosition.js +8 -0
  28. package/dist/flyout/utilities/getRectFromCoordinates.d.ts +6 -0
  29. package/dist/flyout/utilities/getRectFromCoordinates.js +18 -0
  30. package/dist/flyout/utilities/isFullyVisible.d.ts +13 -0
  31. package/dist/flyout/utilities/isFullyVisible.js +28 -0
  32. package/dist/flyout/utilities/tests/applyPosition.test.d.ts +1 -0
  33. package/dist/flyout/utilities/tests/applyPosition.test.js +143 -0
  34. package/dist/flyout/utilities/tests/calculatePosition.test.d.ts +1 -0
  35. package/dist/flyout/utilities/tests/calculatePosition.test.js +536 -0
  36. package/dist/flyout/utilities/tests/centerBySize.test.d.ts +1 -0
  37. package/dist/flyout/utilities/tests/centerBySize.test.js +10 -0
  38. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.d.ts +1 -0
  39. package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +46 -0
  40. package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.d.ts +1 -0
  41. package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.js +66 -0
  42. package/dist/flyout/utilities/tests/getPositionFallbacks.test.d.ts +1 -0
  43. package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +114 -0
  44. package/dist/flyout/utilities/tests/getRTLPosition.test.d.ts +1 -0
  45. package/dist/flyout/utilities/tests/getRTLPosition.test.js +19 -0
  46. package/dist/flyout/utilities/tests/isFullyVisible.test.d.ts +1 -0
  47. package/dist/flyout/utilities/tests/isFullyVisible.test.js +129 -0
  48. package/dist/helpers/rafThrottle.d.ts +2 -0
  49. package/dist/helpers/rafThrottle.js +15 -0
  50. package/dist/helpers/tests/rafThrottle.test.d.ts +1 -0
  51. package/dist/helpers/tests/rafThrottle.test.js +49 -0
  52. package/dist/i18n/isRTL.d.ts +2 -0
  53. package/dist/i18n/isRTL.js +10 -0
  54. package/dist/i18n/tests/isRTL.test.d.ts +1 -0
  55. package/dist/i18n/tests/isRTL.test.js +51 -0
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.js +1 -0
  58. package/package.json +42 -0
@@ -0,0 +1,536 @@
1
+ import { expect, test, describe, vi } from "vitest";
2
+ import { CONTAINER_OFFSET } from "../../constants.js";
3
+ import calculatePosition from "../calculatePosition.js";
4
+ describe("flyout/calculatePosition", () => {
5
+ const createBounds = (left, top, width, height) => {
6
+ return {
7
+ left,
8
+ top,
9
+ width,
10
+ height,
11
+ right: left + width,
12
+ bottom: top + height,
13
+ x: left,
14
+ y: top,
15
+ toJSON: vi.fn(),
16
+ };
17
+ };
18
+ test("calculates position for top placement", () => {
19
+ const triggerBounds = createBounds(100, 200, 50, 30);
20
+ const flyoutBounds = createBounds(0, 0, 40, 60);
21
+ const containerBounds = createBounds(0, 0, 1000, 800);
22
+ const result = calculatePosition({
23
+ triggerBounds,
24
+ flyoutBounds,
25
+ containerBounds,
26
+ position: "top",
27
+ rtl: false,
28
+ contentGap: 0,
29
+ contentShift: 0,
30
+ });
31
+ // top position: above trigger, centered horizontally
32
+ // relativeTop = 200 - 0 = 200, top = 200 - 60 = 140
33
+ // relativeLeft = 100 - 0 = 100, left = 100 + centerBySize(50, 40) = 105
34
+ expect(result.boundaries.top).toBe(140);
35
+ expect(result.boundaries.left).toBe(105);
36
+ // top position sets bottom, so top style is null and bottom is "0px"
37
+ expect(result.styles.top).toBe(null);
38
+ expect(result.styles.bottom).toBe("0px");
39
+ expect(result.styles.left).toBe("0px");
40
+ expect(result.styles.right).toBe(null);
41
+ });
42
+ test("calculates position for bottom, start, and end placements", () => {
43
+ const triggerBounds = createBounds(100, 200, 50, 30);
44
+ const flyoutBounds = createBounds(0, 0, 40, 60);
45
+ const containerBounds = createBounds(0, 0, 1000, 800);
46
+ const bottom = calculatePosition({
47
+ triggerBounds,
48
+ flyoutBounds,
49
+ containerBounds,
50
+ position: "bottom",
51
+ rtl: false,
52
+ contentGap: 0,
53
+ contentShift: 0,
54
+ });
55
+ // relativeTop = 200, top = 200 + 30 = 230
56
+ // relativeLeft = 100, left = 100 + centerBySize(50, 40) = 105
57
+ expect(bottom.boundaries.top).toBe(230);
58
+ expect(bottom.boundaries.left).toBe(105);
59
+ const start = calculatePosition({
60
+ triggerBounds,
61
+ flyoutBounds,
62
+ containerBounds,
63
+ position: "start",
64
+ rtl: false,
65
+ contentGap: 0,
66
+ contentShift: 0,
67
+ });
68
+ // relativeLeft = 100, left = 100 - 40 = 60
69
+ // relativeTop = 200, top = 200 + centerBySize(30, 60) = 200 - 15 = 185
70
+ expect(start.boundaries.left).toBe(60);
71
+ expect(start.boundaries.top).toBe(185);
72
+ const end = calculatePosition({
73
+ triggerBounds,
74
+ flyoutBounds,
75
+ containerBounds,
76
+ position: "end",
77
+ rtl: false,
78
+ contentGap: 0,
79
+ contentShift: 0,
80
+ });
81
+ // relativeLeft = 100, left = 100 + 50 = 150
82
+ // relativeTop = 200, top = 200 + centerBySize(30, 60) = 185
83
+ expect(end.boundaries.left).toBe(150);
84
+ expect(end.boundaries.top).toBe(185);
85
+ });
86
+ test("calculates position for top-start, top-end, bottom-start, and bottom-end", () => {
87
+ const triggerBounds = createBounds(100, 200, 50, 30);
88
+ const flyoutBounds = createBounds(0, 0, 40, 60);
89
+ const containerBounds = createBounds(0, 0, 1000, 800);
90
+ const topStart = calculatePosition({
91
+ triggerBounds,
92
+ flyoutBounds,
93
+ containerBounds,
94
+ position: "top-start",
95
+ rtl: false,
96
+ contentGap: 0,
97
+ contentShift: 0,
98
+ });
99
+ // relativeTop = 200, top = 200 - 60 = 140
100
+ // relativeLeft = 100, left = 100
101
+ expect(topStart.boundaries.top).toBe(140);
102
+ expect(topStart.boundaries.left).toBe(100);
103
+ const topEnd = calculatePosition({
104
+ triggerBounds,
105
+ flyoutBounds,
106
+ containerBounds,
107
+ position: "top-end",
108
+ rtl: false,
109
+ contentGap: 0,
110
+ contentShift: 0,
111
+ });
112
+ // relativeTop = 200, top = 200 - 60 = 140
113
+ // relativeLeft = 100, left = 100 + 50 - 40 = 110
114
+ // relativeRight = 1000 - 150 = 850, right = 850 - 0 = 850 (internal, not in boundaries)
115
+ expect(topEnd.boundaries.top).toBe(140);
116
+ expect(topEnd.boundaries.left).toBe(110);
117
+ const bottomStart = calculatePosition({
118
+ triggerBounds,
119
+ flyoutBounds,
120
+ containerBounds,
121
+ position: "bottom-start",
122
+ rtl: false,
123
+ contentGap: 0,
124
+ contentShift: 0,
125
+ });
126
+ // relativeTop = 200, top = 200 + 30 = 230
127
+ // relativeLeft = 100, left = 100
128
+ expect(bottomStart.boundaries.top).toBe(230);
129
+ expect(bottomStart.boundaries.left).toBe(100);
130
+ const bottomEnd = calculatePosition({
131
+ triggerBounds,
132
+ flyoutBounds,
133
+ containerBounds,
134
+ position: "bottom-end",
135
+ rtl: false,
136
+ contentGap: 0,
137
+ contentShift: 0,
138
+ });
139
+ // relativeTop = 200, top = 200 + 30 = 230
140
+ // relativeLeft = 100, left = 100 + 50 - 40 = 110
141
+ expect(bottomEnd.boundaries.top).toBe(230);
142
+ expect(bottomEnd.boundaries.left).toBe(110);
143
+ });
144
+ test("calculates position for start-top, start-bottom, end-top, and end-bottom", () => {
145
+ const triggerBounds = createBounds(100, 200, 50, 30);
146
+ const flyoutBounds = createBounds(0, 0, 40, 60);
147
+ const containerBounds = createBounds(0, 0, 1000, 800);
148
+ const startTop = calculatePosition({
149
+ triggerBounds,
150
+ flyoutBounds,
151
+ containerBounds,
152
+ position: "start-top",
153
+ rtl: false,
154
+ contentGap: 0,
155
+ contentShift: 0,
156
+ });
157
+ // relativeLeft = 100, left = 100 - 40 = 60
158
+ // relativeTop = 200, top = 200
159
+ expect(startTop.boundaries.left).toBe(60);
160
+ expect(startTop.boundaries.top).toBe(200);
161
+ const startBottom = calculatePosition({
162
+ triggerBounds,
163
+ flyoutBounds,
164
+ containerBounds,
165
+ position: "start-bottom",
166
+ rtl: false,
167
+ contentGap: 0,
168
+ contentShift: 0,
169
+ });
170
+ // relativeLeft = 100, left = 100 - 40 = 60
171
+ // relativeTop = 200, top = 200 + 30 - 60 = 170
172
+ // relativeRight = 1000 - 150 = 850, right = 850 - 0 = 850 (internal)
173
+ expect(startBottom.boundaries.left).toBe(60);
174
+ expect(startBottom.boundaries.top).toBe(170);
175
+ const endTop = calculatePosition({
176
+ triggerBounds,
177
+ flyoutBounds,
178
+ containerBounds,
179
+ position: "end-top",
180
+ rtl: false,
181
+ contentGap: 0,
182
+ contentShift: 0,
183
+ });
184
+ // relativeLeft = 100, left = 100 + 50 = 150
185
+ // relativeTop = 200, top = 200
186
+ expect(endTop.boundaries.left).toBe(150);
187
+ expect(endTop.boundaries.top).toBe(200);
188
+ const endBottom = calculatePosition({
189
+ triggerBounds,
190
+ flyoutBounds,
191
+ containerBounds,
192
+ position: "end-bottom",
193
+ rtl: false,
194
+ contentGap: 0,
195
+ contentShift: 0,
196
+ });
197
+ // relativeLeft = 100, left = 100 + 50 = 150
198
+ // relativeTop = 200, top = 200 + 30 - 60 = 170
199
+ expect(endBottom.boundaries.left).toBe(150);
200
+ expect(endBottom.boundaries.top).toBe(170);
201
+ });
202
+ test("applies contentGap correctly", () => {
203
+ const triggerBounds = createBounds(100, 200, 50, 30);
204
+ const flyoutBounds = createBounds(0, 0, 40, 60);
205
+ const containerBounds = createBounds(0, 0, 1000, 800);
206
+ const top = calculatePosition({
207
+ triggerBounds,
208
+ flyoutBounds,
209
+ containerBounds,
210
+ position: "top",
211
+ rtl: false,
212
+ contentGap: 10,
213
+ contentShift: 0,
214
+ });
215
+ // relativeTop = 200, top = 200 - 60 - 10 = 130
216
+ expect(top.boundaries.top).toBe(130);
217
+ const bottom = calculatePosition({
218
+ triggerBounds,
219
+ flyoutBounds,
220
+ containerBounds,
221
+ position: "bottom",
222
+ rtl: false,
223
+ contentGap: 10,
224
+ contentShift: 0,
225
+ });
226
+ // relativeTop = 200, top = 200 + 30 + 10 = 240
227
+ expect(bottom.boundaries.top).toBe(240);
228
+ const start = calculatePosition({
229
+ triggerBounds,
230
+ flyoutBounds,
231
+ containerBounds,
232
+ position: "start",
233
+ rtl: false,
234
+ contentGap: 10,
235
+ contentShift: 0,
236
+ });
237
+ // relativeLeft = 100, left = 100 - 40 - 10 = 50
238
+ // relativeRight = 850, right = 850 + 50 + 10 = 910 (internal)
239
+ expect(start.boundaries.left).toBe(50);
240
+ const end = calculatePosition({
241
+ triggerBounds,
242
+ flyoutBounds,
243
+ containerBounds,
244
+ position: "end",
245
+ rtl: false,
246
+ contentGap: 10,
247
+ contentShift: 0,
248
+ });
249
+ // relativeLeft = 100, left = 100 + 50 + 10 = 160
250
+ expect(end.boundaries.left).toBe(160);
251
+ });
252
+ test("applies contentShift correctly", () => {
253
+ const triggerBounds = createBounds(100, 200, 50, 30);
254
+ const flyoutBounds = createBounds(0, 0, 40, 60);
255
+ const containerBounds = createBounds(0, 0, 1000, 800);
256
+ const top = calculatePosition({
257
+ triggerBounds,
258
+ flyoutBounds,
259
+ containerBounds,
260
+ position: "top",
261
+ rtl: false,
262
+ contentGap: 0,
263
+ contentShift: 5,
264
+ });
265
+ // relativeLeft = 100, left = 100 + centerBySize(50, 40) + 5 = 100 + 5 + 5 = 110
266
+ expect(top.boundaries.left).toBe(110);
267
+ const topStart = calculatePosition({
268
+ triggerBounds,
269
+ flyoutBounds,
270
+ containerBounds,
271
+ position: "top-start",
272
+ rtl: false,
273
+ contentGap: 0,
274
+ contentShift: 5,
275
+ });
276
+ // relativeLeft = 100, left = 100 + 5 = 105
277
+ expect(topStart.boundaries.left).toBe(105);
278
+ const start = calculatePosition({
279
+ triggerBounds,
280
+ flyoutBounds,
281
+ containerBounds,
282
+ position: "start",
283
+ rtl: false,
284
+ contentGap: 0,
285
+ contentShift: 5,
286
+ });
287
+ // relativeTop = 200, top = 200 + centerBySize(30, 60) + 5 = 200 - 15 + 5 = 190
288
+ expect(start.boundaries.top).toBe(190);
289
+ });
290
+ test("handles RTL by converting positions", () => {
291
+ const triggerBounds = createBounds(100, 200, 50, 30);
292
+ const flyoutBounds = createBounds(0, 0, 40, 60);
293
+ const containerBounds = createBounds(0, 0, 1000, 800);
294
+ // In RTL, "start" should behave like "end" in LTR
295
+ const startRTL = calculatePosition({
296
+ triggerBounds,
297
+ flyoutBounds,
298
+ containerBounds,
299
+ position: "start",
300
+ rtl: true,
301
+ contentGap: 0,
302
+ contentShift: 0,
303
+ });
304
+ const endLTR = calculatePosition({
305
+ triggerBounds,
306
+ flyoutBounds,
307
+ containerBounds,
308
+ position: "end",
309
+ rtl: false,
310
+ contentGap: 0,
311
+ contentShift: 0,
312
+ });
313
+ expect(startRTL.boundaries.left).toBe(endLTR.boundaries.left);
314
+ expect(startRTL.position).toBe("end");
315
+ // In RTL, "top-start" should behave like "top-end" in LTR
316
+ const topStartRTL = calculatePosition({
317
+ triggerBounds,
318
+ flyoutBounds,
319
+ containerBounds,
320
+ position: "top-start",
321
+ rtl: true,
322
+ contentGap: 0,
323
+ contentShift: 0,
324
+ });
325
+ const topEndLTR = calculatePosition({
326
+ triggerBounds,
327
+ flyoutBounds,
328
+ containerBounds,
329
+ position: "top-end",
330
+ rtl: false,
331
+ contentGap: 0,
332
+ contentShift: 0,
333
+ });
334
+ expect(topStartRTL.boundaries.left).toBe(topEndLTR.boundaries.left);
335
+ expect(topStartRTL.position).toBe("top-end");
336
+ });
337
+ test("handles container scrolling", () => {
338
+ const triggerBounds = createBounds(100, 200, 50, 30);
339
+ const flyoutBounds = createBounds(0, 0, 40, 60);
340
+ const containerBounds = createBounds(0, 0, 1000, 800);
341
+ const container = {
342
+ scrollLeft: 50,
343
+ scrollTop: 100,
344
+ clientWidth: 1000,
345
+ clientHeight: 800,
346
+ };
347
+ const result = calculatePosition({
348
+ triggerBounds,
349
+ flyoutBounds,
350
+ containerBounds,
351
+ passedContainer: container,
352
+ position: "top",
353
+ rtl: false,
354
+ contentGap: 0,
355
+ contentShift: 0,
356
+ });
357
+ // With container scroll, relativeLeft should account for scrollLeft
358
+ // relativeLeft = triggerBounds.left - containerBounds.left + containerX
359
+ // = 100 - 0 + 50 = 150
360
+ // So left = 150 + centerBySize(50, 40) = 155
361
+ expect(result.boundaries.left).toBe(150 + 5);
362
+ });
363
+ test("handles window scrolling when no container is provided", () => {
364
+ Object.defineProperty(window, "scrollX", { value: 100, writable: true });
365
+ Object.defineProperty(window, "scrollY", { value: 200, writable: true });
366
+ const triggerBounds = createBounds(100, 200, 50, 30);
367
+ const flyoutBounds = createBounds(0, 0, 40, 60);
368
+ const containerBounds = createBounds(0, 0, 1000, 800);
369
+ const result = calculatePosition({
370
+ triggerBounds,
371
+ flyoutBounds,
372
+ containerBounds,
373
+ position: "top",
374
+ rtl: false,
375
+ contentGap: 0,
376
+ contentShift: 0,
377
+ });
378
+ // Without container, containerX/Y are undefined, so relativeLeft uses 0
379
+ // relativeLeft = triggerBounds.left - containerBounds.left + 0 = 100
380
+ // left = 100 + centerBySize(50, 40) = 105
381
+ // Window scrollX/Y only affect overflow calculations, not position calculations
382
+ expect(result.boundaries.left).toBe(105);
383
+ Object.defineProperty(window, "scrollX", { value: 0, writable: true });
384
+ Object.defineProperty(window, "scrollY", { value: 0, writable: true });
385
+ });
386
+ test("calculates transform correctly for left and right positioning", () => {
387
+ const triggerBounds = createBounds(100, 200, 50, 30);
388
+ const flyoutBounds = createBounds(0, 0, 40, 60);
389
+ const containerBounds = createBounds(0, 0, 1000, 800);
390
+ const start = calculatePosition({
391
+ triggerBounds,
392
+ flyoutBounds,
393
+ containerBounds,
394
+ position: "start",
395
+ rtl: false,
396
+ contentGap: 0,
397
+ contentShift: 0,
398
+ });
399
+ // start position sets right internally, so translateX should be -right
400
+ // right = relativeRight + triggerWidth = 850 + 50 = 900
401
+ // translateX = -900
402
+ expect(start.styles.right).toBe("0px");
403
+ expect(start.styles.left).toBe(null);
404
+ expect(start.styles.transform).toContain("translate(-900px");
405
+ const end = calculatePosition({
406
+ triggerBounds,
407
+ flyoutBounds,
408
+ containerBounds,
409
+ position: "end",
410
+ rtl: false,
411
+ contentGap: 0,
412
+ contentShift: 0,
413
+ });
414
+ // end position doesn't set right, so translateX should be left
415
+ // left = 150, translateX = 150
416
+ expect(end.styles.right).toBe(null);
417
+ expect(end.styles.left).toBe("0px");
418
+ expect(end.styles.transform).toContain("translate(150px");
419
+ });
420
+ test("calculates transform correctly for top and bottom positioning", () => {
421
+ const triggerTop = 200;
422
+ const triggerHeight = 30;
423
+ const triggerBounds = createBounds(100, triggerTop, 50, triggerHeight);
424
+ const flyoutBounds = createBounds(0, 0, 40, 60);
425
+ const containerBounds = createBounds(0, 0, 1000, 800);
426
+ const top = calculatePosition({
427
+ triggerBounds,
428
+ flyoutBounds,
429
+ containerBounds,
430
+ position: "top",
431
+ rtl: false,
432
+ contentGap: 0,
433
+ contentShift: 0,
434
+ });
435
+ // top position sets bottom internally, so translateY should be -bottom
436
+ // containerBoundsBottom = 800, relativeBottom = 800 - 230 = 570
437
+ expect(top.styles.bottom).toBe("0px");
438
+ expect(top.styles.top).toBe(null);
439
+ expect(top.styles.transform).toBe(`translate(105px, ${triggerTop - window.innerHeight}px)`);
440
+ const bottom = calculatePosition({
441
+ triggerBounds,
442
+ flyoutBounds,
443
+ containerBounds,
444
+ position: "bottom",
445
+ rtl: false,
446
+ contentGap: 0,
447
+ contentShift: 0,
448
+ });
449
+ // bottom position doesn't set bottom, so translateY should be top
450
+ // top = 230, translateY = 230
451
+ expect(bottom.styles.bottom).toBe(null);
452
+ expect(bottom.styles.top).toBe("0px");
453
+ expect(bottom.styles.transform).toBe(`translate(105px, ${triggerTop + triggerHeight}px)`);
454
+ });
455
+ test("handles width option '100%'", () => {
456
+ const triggerBounds = createBounds(100, 200, 50, 30);
457
+ const flyoutBounds = createBounds(0, 0, 40, 60);
458
+ const containerBounds = createBounds(0, 0, 1000, 800);
459
+ const result = calculatePosition({
460
+ triggerBounds,
461
+ flyoutBounds,
462
+ containerBounds,
463
+ position: "top",
464
+ rtl: false,
465
+ width: "100%",
466
+ contentGap: 0,
467
+ contentShift: 0,
468
+ });
469
+ expect(result.boundaries.left).toBe(CONTAINER_OFFSET);
470
+ expect(result.boundaries.width).toBe(window.innerWidth - CONTAINER_OFFSET * 2);
471
+ expect(result.styles.width).toBe(`${window.innerWidth - CONTAINER_OFFSET * 2}px`);
472
+ });
473
+ test("handles width option 'trigger'", () => {
474
+ const triggerBounds = createBounds(100, 200, 50, 30);
475
+ const flyoutBounds = createBounds(0, 0, 40, 60);
476
+ const containerBounds = createBounds(0, 0, 1000, 800);
477
+ const result = calculatePosition({
478
+ triggerBounds,
479
+ flyoutBounds,
480
+ containerBounds,
481
+ position: "top",
482
+ rtl: false,
483
+ width: "trigger",
484
+ contentGap: 0,
485
+ contentShift: 0,
486
+ });
487
+ expect(result.boundaries.width).toBe(50);
488
+ expect(result.styles.width).toBe("50px");
489
+ });
490
+ test("handles fallbackAdjustLayout for vertical positions with horizontal overflow", () => {
491
+ const triggerBounds = createBounds(0, 200, 50, 30); // At left edge
492
+ const flyoutBounds = createBounds(0, 0, 40, 60);
493
+ const containerBounds = createBounds(0, 0, 1000, 800);
494
+ Object.defineProperty(window, "scrollX", { value: 0, writable: true });
495
+ Object.defineProperty(window, "scrollY", { value: 0, writable: true });
496
+ const result = calculatePosition({
497
+ triggerBounds,
498
+ flyoutBounds,
499
+ containerBounds,
500
+ position: "top", // Vertical position - checks horizontal overflow
501
+ rtl: false,
502
+ contentGap: 0,
503
+ contentShift: 0,
504
+ fallbackAdjustLayout: true,
505
+ });
506
+ // left = 0 + centerBySize(50, 40) = 5
507
+ // overflow.left = -5 + 0 + 8 = 3 > 0, so left = 8
508
+ expect(result.boundaries.left).toBe(CONTAINER_OFFSET);
509
+ });
510
+ test("handles fallbackAdjustLayout for vertical positions with overflow and height adjustment", () => {
511
+ const triggerBounds = createBounds(100, 5, 50, 30); // Very close to top
512
+ const flyoutBounds = createBounds(0, 0, 40, 200); // Large flyout
513
+ const containerBounds = createBounds(0, 0, 1000, 800);
514
+ Object.defineProperty(window, "scrollX", { value: 0, writable: true });
515
+ Object.defineProperty(window, "scrollY", { value: 0, writable: true });
516
+ const result = calculatePosition({
517
+ triggerBounds,
518
+ flyoutBounds,
519
+ containerBounds,
520
+ position: "top",
521
+ rtl: false,
522
+ contentGap: 0,
523
+ contentShift: 0,
524
+ fallbackAdjustLayout: true,
525
+ fallbackMinHeight: "50",
526
+ });
527
+ // Should adjust height when overflowing top
528
+ if (result.boundaries.top < CONTAINER_OFFSET) {
529
+ expect(result.styles.height).not.toBe(null);
530
+ if (result.styles.height) {
531
+ const height = parseInt(result.styles.height);
532
+ expect(height).toBeGreaterThanOrEqual(50); // fallbackMinHeight
533
+ }
534
+ }
535
+ });
536
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import centerBySize from "../centerBySize.js";
3
+ describe("flyout/centerBySize", () => {
4
+ test("centers even value", () => {
5
+ expect(centerBySize(100, 50)).toEqual(25);
6
+ });
7
+ test("centers odd", () => {
8
+ expect(centerBySize(100, 25)).toEqual(37);
9
+ });
10
+ });
@@ -0,0 +1,46 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import findClosestFixedContainer from "../findClosestFixedContainer.js";
3
+ describe("flyout/findClosestFixedContainer", () => {
4
+ test("returns document.body when element is null", () => {
5
+ const result = findClosestFixedContainer({ el: null });
6
+ expect(result).toBe(document.body);
7
+ });
8
+ test("returns document.body when element is document.body", () => {
9
+ const result = findClosestFixedContainer({ el: document.body });
10
+ expect(result).toBe(document.body);
11
+ });
12
+ test("returns the element itself when it has position fixed", () => {
13
+ const fixedEl = document.createElement("div");
14
+ fixedEl.style.position = "fixed";
15
+ document.body.appendChild(fixedEl);
16
+ const result = findClosestFixedContainer({ el: fixedEl });
17
+ expect(result).toBe(fixedEl);
18
+ });
19
+ test("returns the element itself when it has position sticky", () => {
20
+ const stickyEl = document.createElement("div");
21
+ stickyEl.style.position = "sticky";
22
+ document.body.appendChild(stickyEl);
23
+ const result = findClosestFixedContainer({ el: stickyEl });
24
+ expect(result).toBe(stickyEl);
25
+ });
26
+ test("returns grandparent when it has position fixed", () => {
27
+ const fixedEl = document.createElement("div");
28
+ fixedEl.style.position = "fixed";
29
+ const childEl = document.createElement("div");
30
+ const grandChildEl = document.createElement("div");
31
+ childEl.appendChild(grandChildEl);
32
+ fixedEl.appendChild(childEl);
33
+ document.body.appendChild(fixedEl);
34
+ const result = findClosestFixedContainer({ el: grandChildEl });
35
+ expect(result).toBe(fixedEl);
36
+ });
37
+ test("returns document.body when no fixed container is found", () => {
38
+ const staticEl = document.createElement("div");
39
+ staticEl.style.position = "static";
40
+ const childEl = document.createElement("div");
41
+ staticEl.appendChild(childEl);
42
+ document.body.appendChild(staticEl);
43
+ const result = findClosestFixedContainer({ el: childEl });
44
+ expect(result).toBe(document.body);
45
+ });
46
+ });
@@ -0,0 +1,66 @@
1
+ import { expect, test, describe } from "vitest";
2
+ import findClosestScrollableContainer from "../findClosestScrollableContainer.js";
3
+ describe("flyout/findClosestScrollableContainer", () => {
4
+ test("returns null when element has no parent", () => {
5
+ const result = findClosestScrollableContainer({ el: document.documentElement });
6
+ expect(result).toBe(null);
7
+ });
8
+ test("returns the element itself has overflow auto", () => {
9
+ const scrollableEl = document.createElement("div");
10
+ scrollableEl.style.overflowY = "auto";
11
+ scrollableEl.style.height = "100px";
12
+ const childEl = document.createElement("div");
13
+ childEl.style.height = "200px";
14
+ scrollableEl.appendChild(childEl);
15
+ document.body.appendChild(scrollableEl);
16
+ const result = findClosestScrollableContainer({ el: scrollableEl });
17
+ expect(result).toBe(scrollableEl);
18
+ });
19
+ test("returns the element itself has overflow scroll", () => {
20
+ const scrollableEl = document.createElement("div");
21
+ scrollableEl.style.overflowY = "scroll";
22
+ scrollableEl.style.height = "100px";
23
+ const childEl = document.createElement("div");
24
+ childEl.style.height = "200px";
25
+ scrollableEl.appendChild(childEl);
26
+ document.body.appendChild(scrollableEl);
27
+ const result = findClosestScrollableContainer({ el: scrollableEl });
28
+ expect(result).toBe(scrollableEl);
29
+ });
30
+ test("returns grandparent when it is scrollable", () => {
31
+ const scrollableEl = document.createElement("div");
32
+ scrollableEl.style.overflowY = "auto";
33
+ scrollableEl.style.height = "100px";
34
+ const childEl = document.createElement("div");
35
+ childEl.style.height = "200px";
36
+ const grandChildEl = document.createElement("div");
37
+ grandChildEl.style.height = "100px";
38
+ childEl.appendChild(grandChildEl);
39
+ scrollableEl.appendChild(childEl);
40
+ document.body.appendChild(scrollableEl);
41
+ const result = findClosestScrollableContainer({ el: grandChildEl });
42
+ expect(result).toBe(scrollableEl);
43
+ });
44
+ test("returns null when no scrollable container is found", () => {
45
+ const scrollableEl = document.createElement("div");
46
+ scrollableEl.style.overflowY = "visible";
47
+ scrollableEl.style.height = "100px";
48
+ const childEl = document.createElement("div");
49
+ childEl.style.height = "200px";
50
+ scrollableEl.appendChild(childEl);
51
+ document.body.appendChild(scrollableEl);
52
+ const result = findClosestScrollableContainer({ el: childEl });
53
+ expect(result).toBe(null);
54
+ });
55
+ test("does not return element with overflow auto but scrollHeight <= clientHeight", () => {
56
+ const scrollableEl = document.createElement("div");
57
+ scrollableEl.style.overflowY = "auto";
58
+ scrollableEl.style.height = "100px";
59
+ const childEl = document.createElement("div");
60
+ childEl.style.height = "100px";
61
+ scrollableEl.appendChild(childEl);
62
+ document.body.appendChild(scrollableEl);
63
+ const result = findClosestScrollableContainer({ el: childEl });
64
+ expect(result).toBe(null);
65
+ });
66
+ });