@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.
@@ -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
+ });