@reshaped/utilities 3.9.1-canary.2 → 3.9.1-canary.3
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/flyout/Flyout.js +9 -7
- package/dist/flyout/constants.d.ts +1 -1
- package/dist/flyout/constants.js +1 -1
- package/dist/flyout/utilities/applyPosition.js +45 -25
- package/dist/flyout/utilities/calculateLayoutAdjustment.d.ts +19 -0
- package/dist/flyout/utilities/calculateLayoutAdjustment.js +73 -0
- package/dist/flyout/utilities/calculatePosition.d.ts +7 -20
- package/dist/flyout/utilities/calculatePosition.js +11 -87
- package/dist/flyout/utilities/isFullyVisible.d.ts +2 -4
- package/dist/flyout/utilities/isFullyVisible.js +11 -14
- package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.d.ts +1 -0
- package/dist/flyout/utilities/tests/calculateLayoutAdjustment.test.js +384 -0
- package/dist/flyout/utilities/tests/calculatePosition.test.js +243 -292
- package/dist/flyout/utilities/tests/findClosestFixedContainer.test.js +1 -1
- package/dist/flyout/utilities/tests/isFullyVisible.test.js +27 -51
- package/package.json +1 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { expect, test, describe, vi, beforeEach } from "vitest";
|
|
2
|
+
import { VIEWPORT_OFFSET } from "../../constants.js";
|
|
3
|
+
import calculateLayoutAdjustment from "../calculateLayoutAdjustment.js";
|
|
4
|
+
describe("flyout/calculateLayoutAdjustment", () => {
|
|
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
|
+
beforeEach(() => {
|
|
19
|
+
// Set consistent viewport dimensions for tests
|
|
20
|
+
Object.defineProperty(window, "innerWidth", { value: 1000, writable: true });
|
|
21
|
+
Object.defineProperty(window, "innerHeight", { value: 800, writable: true });
|
|
22
|
+
});
|
|
23
|
+
test("returns styles unchanged when no adjustments needed", () => {
|
|
24
|
+
const result = calculateLayoutAdjustment({
|
|
25
|
+
position: "bottom",
|
|
26
|
+
styles: {
|
|
27
|
+
top: 100,
|
|
28
|
+
left: 200,
|
|
29
|
+
bottom: null,
|
|
30
|
+
right: null,
|
|
31
|
+
},
|
|
32
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
33
|
+
triggerBounds: createBounds(200, 100, 50, 30),
|
|
34
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
35
|
+
fallbackAdjustLayout: false,
|
|
36
|
+
fallbackMinHeight: undefined,
|
|
37
|
+
width: undefined,
|
|
38
|
+
});
|
|
39
|
+
expect(result.position).toBe("bottom");
|
|
40
|
+
expect(result.styles.top).toBe(100);
|
|
41
|
+
expect(result.styles.left).toBe(200);
|
|
42
|
+
expect(result.styles.bottom).toBe(null);
|
|
43
|
+
expect(result.styles.right).toBe(null);
|
|
44
|
+
expect(result.styles.height).toBe(null);
|
|
45
|
+
expect(result.styles.width).toBe(null);
|
|
46
|
+
});
|
|
47
|
+
test("applies width option '100%'", () => {
|
|
48
|
+
const result = calculateLayoutAdjustment({
|
|
49
|
+
position: "bottom",
|
|
50
|
+
styles: {
|
|
51
|
+
top: 100,
|
|
52
|
+
left: 200,
|
|
53
|
+
bottom: null,
|
|
54
|
+
right: null,
|
|
55
|
+
},
|
|
56
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
57
|
+
triggerBounds: createBounds(200, 100, 50, 30),
|
|
58
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
59
|
+
fallbackAdjustLayout: false,
|
|
60
|
+
fallbackMinHeight: undefined,
|
|
61
|
+
width: "100%",
|
|
62
|
+
});
|
|
63
|
+
// left = VIEWPORT_OFFSET = 8
|
|
64
|
+
// width = window.innerWidth - VIEWPORT_OFFSET * 2 = 1000 - 16 = 984
|
|
65
|
+
expect(result.styles.left).toBe(VIEWPORT_OFFSET);
|
|
66
|
+
expect(result.styles.width).toBe(window.innerWidth - VIEWPORT_OFFSET * 2);
|
|
67
|
+
});
|
|
68
|
+
test("applies width option 'trigger'", () => {
|
|
69
|
+
const triggerBounds = createBounds(200, 100, 75, 30);
|
|
70
|
+
const result = calculateLayoutAdjustment({
|
|
71
|
+
position: "bottom",
|
|
72
|
+
styles: {
|
|
73
|
+
top: 100,
|
|
74
|
+
left: 200,
|
|
75
|
+
bottom: null,
|
|
76
|
+
right: null,
|
|
77
|
+
},
|
|
78
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
79
|
+
triggerBounds,
|
|
80
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
81
|
+
fallbackAdjustLayout: false,
|
|
82
|
+
fallbackMinHeight: undefined,
|
|
83
|
+
width: "trigger",
|
|
84
|
+
});
|
|
85
|
+
expect(result.styles.width).toBe(75);
|
|
86
|
+
});
|
|
87
|
+
test("adjusts left when vertical position overflows left edge", () => {
|
|
88
|
+
const result = calculateLayoutAdjustment({
|
|
89
|
+
position: "top",
|
|
90
|
+
styles: {
|
|
91
|
+
top: 100,
|
|
92
|
+
left: 5, // Would overflow left edge (needs to be at least VIEWPORT_OFFSET)
|
|
93
|
+
bottom: 500,
|
|
94
|
+
right: null,
|
|
95
|
+
},
|
|
96
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
97
|
+
triggerBounds: createBounds(5, 100, 50, 30),
|
|
98
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
99
|
+
fallbackAdjustLayout: true,
|
|
100
|
+
fallbackMinHeight: undefined,
|
|
101
|
+
width: undefined,
|
|
102
|
+
});
|
|
103
|
+
// overflow.left = 0 + 8 - 5 = 3 > 0
|
|
104
|
+
// left = VIEWPORT_OFFSET + containerLeft = 8 + 0 = 8
|
|
105
|
+
expect(result.styles.left).toBe(VIEWPORT_OFFSET);
|
|
106
|
+
});
|
|
107
|
+
test("adjusts left when vertical position overflows right edge", () => {
|
|
108
|
+
const result = calculateLayoutAdjustment({
|
|
109
|
+
position: "top",
|
|
110
|
+
styles: {
|
|
111
|
+
top: 100,
|
|
112
|
+
left: 900, // With flyout width 150, would overflow right edge
|
|
113
|
+
bottom: 500,
|
|
114
|
+
right: null,
|
|
115
|
+
},
|
|
116
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
117
|
+
triggerBounds: createBounds(900, 100, 50, 30),
|
|
118
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
119
|
+
fallbackAdjustLayout: true,
|
|
120
|
+
fallbackMinHeight: undefined,
|
|
121
|
+
width: undefined,
|
|
122
|
+
});
|
|
123
|
+
// overflow.right = 900 + 150 + 8 - 0 - 1000 = 58 > 0
|
|
124
|
+
// left = 900 - 58 = 842
|
|
125
|
+
expect(result.styles.left).toBe(842);
|
|
126
|
+
});
|
|
127
|
+
test("adjusts top when horizontal position overflows top edge", () => {
|
|
128
|
+
const result = calculateLayoutAdjustment({
|
|
129
|
+
position: "start",
|
|
130
|
+
styles: {
|
|
131
|
+
top: 5, // Would overflow top edge
|
|
132
|
+
left: 100,
|
|
133
|
+
bottom: null,
|
|
134
|
+
right: 800,
|
|
135
|
+
},
|
|
136
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
137
|
+
triggerBounds: createBounds(100, 5, 50, 30),
|
|
138
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
139
|
+
fallbackAdjustLayout: true,
|
|
140
|
+
fallbackMinHeight: undefined,
|
|
141
|
+
width: undefined,
|
|
142
|
+
});
|
|
143
|
+
// overflow.top = 0 + 8 - 5 = 3 > 0
|
|
144
|
+
// top = containerTop + VIEWPORT_OFFSET = 0 + 8 = 8
|
|
145
|
+
expect(result.styles.top).toBe(VIEWPORT_OFFSET);
|
|
146
|
+
});
|
|
147
|
+
test("adjusts top when horizontal position overflows bottom edge", () => {
|
|
148
|
+
const result = calculateLayoutAdjustment({
|
|
149
|
+
position: "start",
|
|
150
|
+
styles: {
|
|
151
|
+
top: 700, // With flyout height 200, would overflow bottom edge
|
|
152
|
+
left: 100,
|
|
153
|
+
bottom: null,
|
|
154
|
+
right: 800,
|
|
155
|
+
},
|
|
156
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
157
|
+
triggerBounds: createBounds(100, 700, 50, 30),
|
|
158
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
159
|
+
fallbackAdjustLayout: true,
|
|
160
|
+
fallbackMinHeight: undefined,
|
|
161
|
+
width: undefined,
|
|
162
|
+
});
|
|
163
|
+
// overflow.bottom = 700 + 200 + 8 - 0 - 800 = 108 > 0
|
|
164
|
+
// top = 700 - 108 = 592
|
|
165
|
+
expect(result.styles.top).toBe(592);
|
|
166
|
+
});
|
|
167
|
+
test("adjusts bottom value when top overflows for horizontal position with bottom set", () => {
|
|
168
|
+
const result = calculateLayoutAdjustment({
|
|
169
|
+
position: "start-bottom",
|
|
170
|
+
styles: {
|
|
171
|
+
top: 5, // Would overflow top edge
|
|
172
|
+
left: 100,
|
|
173
|
+
bottom: 200,
|
|
174
|
+
right: 800,
|
|
175
|
+
},
|
|
176
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
177
|
+
triggerBounds: createBounds(100, 5, 50, 30),
|
|
178
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
179
|
+
fallbackAdjustLayout: true,
|
|
180
|
+
fallbackMinHeight: undefined,
|
|
181
|
+
width: undefined,
|
|
182
|
+
});
|
|
183
|
+
// overflow.top = 0 + 8 - 5 = 3 > 0
|
|
184
|
+
// top = 8
|
|
185
|
+
// bottom = 200 - 3 = 197
|
|
186
|
+
expect(result.styles.top).toBe(VIEWPORT_OFFSET);
|
|
187
|
+
expect(result.styles.bottom).toBe(197);
|
|
188
|
+
});
|
|
189
|
+
test("adjusts right value when left overflows for vertical position with right set", () => {
|
|
190
|
+
const result = calculateLayoutAdjustment({
|
|
191
|
+
position: "top-end",
|
|
192
|
+
styles: {
|
|
193
|
+
top: 100,
|
|
194
|
+
left: 5, // Would overflow left edge
|
|
195
|
+
bottom: 500,
|
|
196
|
+
right: 995,
|
|
197
|
+
},
|
|
198
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
199
|
+
triggerBounds: createBounds(5, 100, 50, 30),
|
|
200
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
201
|
+
fallbackAdjustLayout: true,
|
|
202
|
+
fallbackMinHeight: undefined,
|
|
203
|
+
width: undefined,
|
|
204
|
+
});
|
|
205
|
+
// overflow.left = 0 + 8 - 5 = 3 > 0
|
|
206
|
+
// left = 8
|
|
207
|
+
// right = 995 - 3 = 992
|
|
208
|
+
expect(result.styles.left).toBe(VIEWPORT_OFFSET);
|
|
209
|
+
expect(result.styles.right).toBe(992);
|
|
210
|
+
});
|
|
211
|
+
test("adjusts height when top overflow persists after position adjustment", () => {
|
|
212
|
+
const result = calculateLayoutAdjustment({
|
|
213
|
+
position: "start",
|
|
214
|
+
styles: {
|
|
215
|
+
top: 5, // Would overflow top edge
|
|
216
|
+
left: 100,
|
|
217
|
+
bottom: null,
|
|
218
|
+
right: 800,
|
|
219
|
+
},
|
|
220
|
+
flyoutBounds: createBounds(0, 0, 150, 250), // Larger height
|
|
221
|
+
triggerBounds: createBounds(100, 5, 50, 30),
|
|
222
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 100 }, // Small container
|
|
223
|
+
fallbackAdjustLayout: true,
|
|
224
|
+
fallbackMinHeight: "50",
|
|
225
|
+
width: undefined,
|
|
226
|
+
});
|
|
227
|
+
// After adjusting top to VIEWPORT_OFFSET (8), there's still overflow
|
|
228
|
+
// updatedOverflow.top = 0 + 8 - 8 = 0 (no overflow)
|
|
229
|
+
// But let's check bottom overflow
|
|
230
|
+
// updatedOverflow.bottom = 8 + 250 + 8 - 0 - 100 = 166 > 0
|
|
231
|
+
// height = max(50, 250 - 166) = max(50, 84) = 84
|
|
232
|
+
expect(result.styles.height).toBeGreaterThanOrEqual(50);
|
|
233
|
+
});
|
|
234
|
+
test("adjusts height when bottom overflow persists after position adjustment", () => {
|
|
235
|
+
const result = calculateLayoutAdjustment({
|
|
236
|
+
position: "end",
|
|
237
|
+
styles: {
|
|
238
|
+
top: 100,
|
|
239
|
+
left: 100,
|
|
240
|
+
bottom: null,
|
|
241
|
+
right: null,
|
|
242
|
+
},
|
|
243
|
+
flyoutBounds: createBounds(0, 0, 150, 750), // Very tall flyout
|
|
244
|
+
triggerBounds: createBounds(100, 100, 50, 30),
|
|
245
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
246
|
+
fallbackAdjustLayout: true,
|
|
247
|
+
fallbackMinHeight: "100",
|
|
248
|
+
width: undefined,
|
|
249
|
+
});
|
|
250
|
+
// overflow.bottom = 100 + 750 + 8 - 0 - 800 = 58 > 0
|
|
251
|
+
// top = 100 - 58 = 42
|
|
252
|
+
// updatedOverflow.bottom = 42 + 750 + 8 - 0 - 800 = 0 (no more overflow)
|
|
253
|
+
// So no height adjustment needed in this case
|
|
254
|
+
// But if there's still overflow, height would be adjusted
|
|
255
|
+
expect(result.styles.top).toBeLessThan(100);
|
|
256
|
+
});
|
|
257
|
+
test("respects fallbackMinHeight when adjusting height", () => {
|
|
258
|
+
const result = calculateLayoutAdjustment({
|
|
259
|
+
position: "start",
|
|
260
|
+
styles: {
|
|
261
|
+
top: 5,
|
|
262
|
+
left: 100,
|
|
263
|
+
bottom: null,
|
|
264
|
+
right: 800,
|
|
265
|
+
},
|
|
266
|
+
flyoutBounds: createBounds(0, 0, 150, 300),
|
|
267
|
+
triggerBounds: createBounds(100, 5, 50, 30),
|
|
268
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 50 }, // Very small container
|
|
269
|
+
fallbackAdjustLayout: true,
|
|
270
|
+
fallbackMinHeight: "150",
|
|
271
|
+
width: undefined,
|
|
272
|
+
});
|
|
273
|
+
// After adjustments, if height is calculated, it should be at least 150
|
|
274
|
+
if (result.styles.height !== null) {
|
|
275
|
+
expect(result.styles.height).toBeGreaterThanOrEqual(150);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
test("handles container with offset", () => {
|
|
279
|
+
const result = calculateLayoutAdjustment({
|
|
280
|
+
position: "top",
|
|
281
|
+
styles: {
|
|
282
|
+
top: 105, // Relative to container at top: 100
|
|
283
|
+
left: 55, // Relative to container at left: 50
|
|
284
|
+
bottom: 500,
|
|
285
|
+
right: null,
|
|
286
|
+
},
|
|
287
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
288
|
+
triggerBounds: createBounds(55, 105, 50, 30),
|
|
289
|
+
containerBounds: { left: 50, top: 100, width: 900, height: 700 },
|
|
290
|
+
fallbackAdjustLayout: true,
|
|
291
|
+
fallbackMinHeight: undefined,
|
|
292
|
+
width: undefined,
|
|
293
|
+
});
|
|
294
|
+
// overflow.left = 50 + 8 - 55 = 3 > 0
|
|
295
|
+
// left = VIEWPORT_OFFSET + containerLeft = 8 + 50 = 58
|
|
296
|
+
expect(result.styles.left).toBe(58);
|
|
297
|
+
});
|
|
298
|
+
test("handles end position (horizontal) with vertical overflow", () => {
|
|
299
|
+
const result = calculateLayoutAdjustment({
|
|
300
|
+
position: "end-top",
|
|
301
|
+
styles: {
|
|
302
|
+
top: 5,
|
|
303
|
+
left: 200,
|
|
304
|
+
bottom: null,
|
|
305
|
+
right: null,
|
|
306
|
+
},
|
|
307
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
308
|
+
triggerBounds: createBounds(200, 5, 50, 30),
|
|
309
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
310
|
+
fallbackAdjustLayout: true,
|
|
311
|
+
fallbackMinHeight: undefined,
|
|
312
|
+
width: undefined,
|
|
313
|
+
});
|
|
314
|
+
// end-top is a horizontal position (starts with "end")
|
|
315
|
+
// overflow.top = 0 + 8 - 5 = 3 > 0
|
|
316
|
+
// top = 0 + 8 = 8
|
|
317
|
+
expect(result.styles.top).toBe(VIEWPORT_OFFSET);
|
|
318
|
+
});
|
|
319
|
+
test("handles bottom position (vertical) with horizontal overflow", () => {
|
|
320
|
+
const result = calculateLayoutAdjustment({
|
|
321
|
+
position: "bottom-start",
|
|
322
|
+
styles: {
|
|
323
|
+
top: 200,
|
|
324
|
+
left: 5,
|
|
325
|
+
bottom: null,
|
|
326
|
+
right: null,
|
|
327
|
+
},
|
|
328
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
329
|
+
triggerBounds: createBounds(5, 200, 50, 30),
|
|
330
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
331
|
+
fallbackAdjustLayout: true,
|
|
332
|
+
fallbackMinHeight: undefined,
|
|
333
|
+
width: undefined,
|
|
334
|
+
});
|
|
335
|
+
// bottom-start is a vertical position (starts with "bottom")
|
|
336
|
+
// overflow.left = 0 + 8 - 5 = 3 > 0
|
|
337
|
+
// left = 8 + 0 = 8
|
|
338
|
+
expect(result.styles.left).toBe(VIEWPORT_OFFSET);
|
|
339
|
+
});
|
|
340
|
+
test("width option overrides layout adjustment for left position", () => {
|
|
341
|
+
const result = calculateLayoutAdjustment({
|
|
342
|
+
position: "top",
|
|
343
|
+
styles: {
|
|
344
|
+
top: 100,
|
|
345
|
+
left: 900, // Would be adjusted due to overflow
|
|
346
|
+
bottom: 500,
|
|
347
|
+
right: null,
|
|
348
|
+
},
|
|
349
|
+
flyoutBounds: createBounds(0, 0, 150, 200),
|
|
350
|
+
triggerBounds: createBounds(900, 100, 50, 30),
|
|
351
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
352
|
+
fallbackAdjustLayout: true,
|
|
353
|
+
fallbackMinHeight: undefined,
|
|
354
|
+
width: "100%",
|
|
355
|
+
});
|
|
356
|
+
// Layout adjustment happens first, but width option overrides left
|
|
357
|
+
// left = VIEWPORT_OFFSET = 8 (from width: "100%")
|
|
358
|
+
// width = 1000 - 16 = 984
|
|
359
|
+
expect(result.styles.left).toBe(VIEWPORT_OFFSET);
|
|
360
|
+
expect(result.styles.width).toBe(984);
|
|
361
|
+
});
|
|
362
|
+
test("adjusts height and updates bottom value when bottom is set", () => {
|
|
363
|
+
const result = calculateLayoutAdjustment({
|
|
364
|
+
position: "end",
|
|
365
|
+
styles: {
|
|
366
|
+
top: 100,
|
|
367
|
+
left: 100,
|
|
368
|
+
bottom: 600,
|
|
369
|
+
right: null,
|
|
370
|
+
},
|
|
371
|
+
flyoutBounds: createBounds(0, 0, 150, 750), // Very tall
|
|
372
|
+
triggerBounds: createBounds(100, 100, 50, 30),
|
|
373
|
+
containerBounds: { left: 0, top: 0, width: 1000, height: 800 },
|
|
374
|
+
fallbackAdjustLayout: true,
|
|
375
|
+
fallbackMinHeight: "50",
|
|
376
|
+
width: undefined,
|
|
377
|
+
});
|
|
378
|
+
// After position adjustment and if there's still overflow:
|
|
379
|
+
// If bottom is set and height is adjusted, bottom should also be updated
|
|
380
|
+
if (result.styles.height !== null) {
|
|
381
|
+
expect(result.styles.bottom).not.toBe(600);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|