@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.
- package/LICENSE.md +21 -0
- package/dist/dom/getShadowRoot.d.ts +2 -0
- package/dist/dom/getShadowRoot.js +5 -0
- package/dist/flyout/Flyout.d.ts +13 -0
- package/dist/flyout/Flyout.js +101 -0
- package/dist/flyout/constants.d.ts +9 -0
- package/dist/flyout/constants.js +9 -0
- package/dist/flyout/index.d.ts +1 -0
- package/dist/flyout/index.js +1 -0
- package/dist/flyout/tests/Flyout.test.d.ts +1 -0
- package/dist/flyout/tests/Flyout.test.js +129 -0
- package/dist/flyout/types.d.ts +24 -0
- package/dist/flyout/types.js +1 -0
- package/dist/flyout/utilities/applyPosition.d.ts +7 -0
- package/dist/flyout/utilities/applyPosition.js +103 -0
- package/dist/flyout/utilities/calculatePosition.d.ts +33 -0
- package/dist/flyout/utilities/calculatePosition.js +159 -0
- package/dist/flyout/utilities/centerBySize.d.ts +5 -0
- package/dist/flyout/utilities/centerBySize.js +7 -0
- package/dist/flyout/utilities/findClosestFixedContainer.d.ts +5 -0
- package/dist/flyout/utilities/findClosestFixedContainer.js +18 -0
- package/dist/flyout/utilities/findClosestScrollableContainer.d.ts +5 -0
- package/dist/flyout/utilities/findClosestScrollableContainer.js +12 -0
- package/dist/flyout/utilities/getPositionFallbacks.d.ts +8 -0
- package/dist/flyout/utilities/getPositionFallbacks.js +43 -0
- package/dist/flyout/utilities/getRTLPosition.d.ts +3 -0
- package/dist/flyout/utilities/getRTLPosition.js +8 -0
- package/dist/flyout/utilities/getRectFromCoordinates.d.ts +6 -0
- package/dist/flyout/utilities/getRectFromCoordinates.js +18 -0
- package/dist/flyout/utilities/isFullyVisible.d.ts +13 -0
- package/dist/flyout/utilities/isFullyVisible.js +28 -0
- package/dist/flyout/utilities/tests/applyPosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/applyPosition.test.js +143 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.js +536 -0
- package/dist/flyout/utilities/tests/centerBySize.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/centerBySize.test.js +10 -0
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +46 -0
- package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/findClosestScrollableContainer.test.js +66 -0
- package/dist/flyout/utilities/tests/getPositionFallbacks.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/getPositionFallbacks.test.js +114 -0
- package/dist/flyout/utilities/tests/getRTLPosition.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/getRTLPosition.test.js +19 -0
- package/dist/flyout/utilities/tests/isFullyVisible.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/isFullyVisible.test.js +129 -0
- package/dist/helpers/rafThrottle.d.ts +2 -0
- package/dist/helpers/rafThrottle.js +15 -0
- package/dist/helpers/tests/rafThrottle.test.d.ts +1 -0
- package/dist/helpers/tests/rafThrottle.test.js +49 -0
- package/dist/i18n/isRTL.d.ts +2 -0
- package/dist/i18n/isRTL.js +10 -0
- package/dist/i18n/tests/isRTL.test.d.ts +1 -0
- package/dist/i18n/tests/isRTL.test.js +51 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|