@fiscozen/composables 0.1.37 → 0.1.38

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.
@@ -7,7 +7,7 @@ export const useFloating = (
7
7
  ): {
8
8
  float: FzRect
9
9
  rect: Ref<DOMRect | undefined>
10
- setPosition: () => Promise<void>
10
+ setPosition: (ignoreCallback?: boolean) => Promise<void>
11
11
  position: Ref<FzFloatingPosition>
12
12
  actualPosition?: Ref<FzFloatingPosition | undefined>
13
13
  openerRect: Ref<DOMRect | undefined>
@@ -38,272 +38,349 @@ export const useFloating = (
38
38
  const floatObserver = ref(new IntersectionObserver(handleIntersect, options))
39
39
  const actualPosition = ref<FzFloatingPosition>()
40
40
 
41
- const setPosition = (ignoreCallback: boolean = false) =>
42
- nextTick(() => {
43
- actualPosition.value = args.position ? args.position.value : 'auto'
44
- safeElementDomRef.value = (
45
- typeof args.element.value.domRef.value === 'string'
46
- ? document.querySelector(args.element.value.domRef.value)
47
- : args.element.value.domRef.value
41
+ // Helper: Resolve DOM references
42
+ const resolveDomReferences = () => {
43
+ safeElementDomRef.value = (
44
+ typeof args.element.value.domRef.value === 'string'
45
+ ? document.querySelector(args.element.value.domRef.value)
46
+ : args.element.value.domRef.value
47
+ ) as HTMLElement
48
+
49
+ if (!args.container?.value) {
50
+ safeContainerDomRef.value = document.body
51
+ } else {
52
+ safeContainerDomRef.value = (
53
+ typeof args.container.value?.domRef.value === 'string'
54
+ ? document.querySelector(args.container.value.domRef.value)
55
+ : args.container.value?.domRef.value
48
56
  ) as HTMLElement
49
- if (!args.container?.value) {
50
- safeContainerDomRef.value = document.body
51
- } else {
52
- safeContainerDomRef.value = (
53
- typeof args.container.value?.domRef.value === 'string'
54
- ? document.querySelector(args.container.value.domRef.value)
55
- : args.container.value?.domRef.value
56
- ) as HTMLElement
57
- safeContainerDomRef.value ??= document.body
57
+ safeContainerDomRef.value ??= document.body
58
+ }
59
+
60
+ if (!safeElementDomRef.value) {
61
+ throw new Error('missing reference element for floating behavior')
62
+ }
63
+
64
+ if (args.opener?.value) {
65
+ safeOpenerDomRef.value = (
66
+ typeof args.opener.value?.domRef.value === 'string'
67
+ ? document.querySelector(args.opener.value.domRef.value)
68
+ : args.opener.value?.domRef.value
69
+ ) as HTMLElement
70
+ }
71
+
72
+ rect.value = safeElementDomRef.value.getBoundingClientRect()
73
+ openerRect.value = safeOpenerDomRef.value?.getBoundingClientRect()
74
+ containerRect.value = safeContainerDomRef.value.getBoundingClientRect()
75
+ }
76
+
77
+ // Helper: Resolve auto position to actual position
78
+ const resolveAutoPositionValue = () => {
79
+ if (!args.opener?.value || !safeOpenerDomRef.value) return
80
+
81
+ switch (actualPosition.value) {
82
+ case 'auto':
83
+ actualPosition.value = getHighestAvailableSpacePos(
84
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
85
+ safeElementDomRef.value as HTMLElement,
86
+ safeOpenerDomRef.value as HTMLElement
87
+ )
88
+ break
89
+ case 'auto-vertical':
90
+ actualPosition.value = getHighestAvailableSpacePos(
91
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
92
+ safeElementDomRef.value as HTMLElement,
93
+ safeOpenerDomRef.value as HTMLElement,
94
+ undefined,
95
+ true
96
+ )
97
+ break
98
+ case 'auto-start':
99
+ actualPosition.value = getHighestAvailableSpacePos(
100
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
101
+ safeElementDomRef.value as HTMLElement,
102
+ safeOpenerDomRef.value as HTMLElement,
103
+ 'start'
104
+ )
105
+ break
106
+ case 'auto-vertical-start':
107
+ actualPosition.value = getHighestAvailableSpacePos(
108
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
109
+ safeElementDomRef.value as HTMLElement,
110
+ safeOpenerDomRef.value as HTMLElement,
111
+ 'start',
112
+ true
113
+ )
114
+ break
115
+ case 'auto-end':
116
+ actualPosition.value = getHighestAvailableSpacePos(
117
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
118
+ safeElementDomRef.value as HTMLElement,
119
+ safeOpenerDomRef.value as HTMLElement,
120
+ 'end'
121
+ )
122
+ break
123
+ case 'auto-vertical-end':
124
+ actualPosition.value = getHighestAvailableSpacePos(
125
+ args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
126
+ safeElementDomRef.value as HTMLElement,
127
+ safeOpenerDomRef.value as HTMLElement,
128
+ 'end',
129
+ true
130
+ )
131
+ break
132
+ default:
133
+ break
134
+ }
135
+ }
136
+
137
+ // Helper: Calculate position with opener
138
+ const calculatePositionWithOpener = (
139
+ elStyle: CSSStyleDeclaration,
140
+ translateX: { value: number },
141
+ translateY: { value: number }
142
+ ) => {
143
+ if (!openerRect.value) return
144
+
145
+ const leftWithoutXMargin =
146
+ openerRect.value.left - parseFloat(elStyle.marginLeft) - parseFloat(elStyle.marginRight)
147
+ const leftWithoutLeftMargin = openerRect.value.left - parseFloat(elStyle.marginLeft)
148
+ const topWithoutYMargin =
149
+ openerRect.value.top - parseFloat(elStyle.marginTop) - parseFloat(elStyle.marginBottom)
150
+ const topWithoutTopMargin = openerRect.value.top - parseFloat(elStyle.marginTop)
151
+
152
+ switch (actualPosition.value) {
153
+ case 'bottom':
154
+ float.position.y = openerRect.value.bottom
155
+ float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
156
+ translateX.value = -50
157
+ translateY.value = 0
158
+ break
159
+ case 'bottom-start':
160
+ float.position.y = openerRect.value.bottom
161
+ float.position.x = leftWithoutXMargin
162
+ translateX.value = 0
163
+ translateY.value = 0
164
+ break
165
+ case 'bottom-end':
166
+ float.position.y = openerRect.value.bottom
167
+ float.position.x = openerRect.value.right
168
+ translateX.value = -100
169
+ translateY.value = 0
170
+ break
171
+ case 'left-start':
172
+ float.position.y = topWithoutYMargin
173
+ float.position.x = leftWithoutXMargin
174
+ translateX.value = -100
175
+ translateY.value = 0
176
+ break
177
+ case 'left':
178
+ float.position.y = topWithoutTopMargin + openerRect.value.height / 2
179
+ float.position.x = leftWithoutXMargin
180
+ translateY.value = -50
181
+ translateX.value = -100
182
+ break
183
+ case 'left-end':
184
+ float.position.y = openerRect.value.bottom
185
+ float.position.x = leftWithoutXMargin
186
+ translateY.value = -100
187
+ translateX.value = -100
188
+ break
189
+ case 'top-start':
190
+ float.position.y = topWithoutYMargin
191
+ float.position.x = leftWithoutXMargin
192
+ translateY.value = -100
193
+ translateX.value = 0
194
+ break
195
+ case 'top':
196
+ float.position.y = topWithoutYMargin
197
+ float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
198
+ translateX.value = -50
199
+ translateY.value = -100
200
+ break
201
+ case 'top-end':
202
+ float.position.y = topWithoutYMargin
203
+ float.position.x = openerRect.value.right
204
+ translateX.value = -100
205
+ translateY.value = -100
206
+ break
207
+ case 'right-start':
208
+ float.position.y = topWithoutYMargin
209
+ float.position.x = openerRect.value.right
210
+ translateX.value = 0
211
+ translateY.value = 0
212
+ break
213
+ case 'right':
214
+ float.position.y = topWithoutTopMargin + openerRect.value.height / 2
215
+ float.position.x = openerRect.value.right
216
+ translateY.value = -50
217
+ translateX.value = 0
218
+ break
219
+ case 'right-end':
220
+ float.position.y = openerRect.value.bottom
221
+ float.position.x = openerRect.value.right
222
+ translateY.value = -100
223
+ translateX.value = 0
224
+ break
225
+ default:
226
+ break
227
+ }
228
+ }
229
+
230
+ // Helper: Calculate position without opener
231
+ const calculatePositionWithoutOpener = (translateX: { value: number }) => {
232
+ if (!containerRect.value || !rect.value) return
233
+
234
+ switch (actualPosition.value) {
235
+ case 'bottom':
236
+ float.position.y = containerRect.value.bottom - rect.value.height
237
+ float.position.x = containerRect.value.left + containerRect.value.width / 2
238
+ translateX.value = -50
239
+ break
240
+ case 'left-end':
241
+ case 'bottom-start':
242
+ float.position.y = containerRect.value.bottom - rect.value.height
243
+ float.position.x = containerRect.value.left
244
+ break
245
+ case 'right-end':
246
+ case 'bottom-end':
247
+ float.position.y = containerRect.value.bottom - rect.value.height
248
+ float.position.x = containerRect.value.right - rect.value.width
249
+ break
250
+ case 'left':
251
+ float.position.y =
252
+ containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
253
+ float.position.x = containerRect.value.left
254
+ break
255
+ case 'top-start':
256
+ case 'left-start':
257
+ float.position.y = containerRect.value.top
258
+ float.position.x = containerRect.value.left
259
+ break
260
+ case 'top':
261
+ float.position.y = containerRect.value.top
262
+ float.position.x =
263
+ containerRect.value.left + (containerRect.value.width - rect.value.width) / 2
264
+ break
265
+ case 'top-end':
266
+ case 'right-start':
267
+ float.position.y = containerRect.value.top
268
+ float.position.x = containerRect.value.right - rect.value.width
269
+ break
270
+ case 'right':
271
+ float.position.y =
272
+ containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
273
+ float.position.x = containerRect.value.right - rect.value.width
274
+ break
275
+ default:
276
+ break
277
+ }
278
+ }
279
+
280
+ // Helper: Apply boundary corrections
281
+ const applyBoundaryCorrections = (
282
+ realPos: { x: number; y: number },
283
+ translateX: { value: number },
284
+ translateY: { value: number }
285
+ ) => {
286
+ if (!containerRect.value || !rect.value) return
287
+
288
+ if (realPos.x < containerRect.value.left) {
289
+ float.position.x = containerRect.value.left
290
+ translateX.value = 0
291
+ }
292
+
293
+ if (realPos.x + rect.value.width > containerRect.value.right) {
294
+ const fixVal = containerRect.value.right - rect.value.width
295
+ if (fixVal > 0) {
296
+ float.position.x = fixVal
58
297
  }
298
+ translateX.value = 0
299
+ }
300
+
301
+ if (realPos.y < containerRect.value.top) {
302
+ float.position.y = containerRect.value.top
303
+ translateY.value = 0
304
+ }
59
305
 
60
- if (!safeElementDomRef.value) {
61
- throw new Error('missing reference element for floating behavior')
306
+ if (realPos.y + rect.value.height > containerRect.value.bottom) {
307
+ const fixVal = containerRect.value.bottom - rect.value.height
308
+ if (fixVal > 0) {
309
+ float.position.y = fixVal
62
310
  }
311
+ translateY.value = 0
312
+ }
313
+ }
314
+
315
+ // Helper: Apply element styles
316
+ const applyElementStyles = () => {
317
+ if (!safeElementDomRef.value) return
318
+
319
+ safeElementDomRef.value.style.top = `${float.position.y}px`
320
+ safeElementDomRef.value.style.left = `${float.position.x}px`
321
+ safeElementDomRef.value.style.position = 'fixed'
322
+ safeElementDomRef.value.style.display = 'flex'
323
+ }
324
+
325
+ const setPosition = (ignoreCallback: boolean = false) =>
326
+ nextTick(() => {
327
+ actualPosition.value = args.position ? args.position.value : 'auto'
328
+
329
+ // Step 1: Resolve DOM references
330
+ resolveDomReferences()
63
331
 
64
- if (args.opener?.value) {
65
- safeOpenerDomRef.value = (
66
- typeof args.opener.value?.domRef.value === 'string'
67
- ? document.querySelector(args.opener.value.domRef.value)
68
- : args.opener.value?.domRef.value
69
- ) as HTMLElement
332
+ // Step 1.5: CRITICAL - Remove element from document flow immediately
333
+ // This prevents the floating element from pushing the opener and causing layout shift
334
+ if (safeElementDomRef.value) {
335
+ safeElementDomRef.value.style.position = 'fixed'
336
+ safeElementDomRef.value.style.top = '0px'
337
+ safeElementDomRef.value.style.left = '0px'
70
338
  }
71
339
 
72
- rect.value = safeElementDomRef.value.getBoundingClientRect()
340
+ // Recalculate all rects after content is rendered
341
+ rect.value = safeElementDomRef.value!.getBoundingClientRect()
73
342
  openerRect.value = safeOpenerDomRef.value?.getBoundingClientRect()
74
- containerRect.value = safeContainerDomRef.value.getBoundingClientRect()
343
+ containerRect.value = safeContainerDomRef.value!.getBoundingClientRect()
75
344
 
76
345
  const elStyle = window.getComputedStyle(safeElementDomRef.value as HTMLElement)
77
346
 
78
- // multiple observer calls on same target do not cause multiple registrations
347
+ // Step 2: Setup intersection observer
79
348
  floatObserver.value.observe(safeElementDomRef.value as HTMLElement)
80
349
  floatObserver.value.observe(safeContainerDomRef.value as HTMLElement)
81
350
 
82
- let translateY = 0
83
- let translateX = 0
351
+ // Step 3: Resolve auto positions
352
+ resolveAutoPositionValue()
353
+
354
+ // Step 4: Calculate position based on opener presence
355
+ const translate = { x: { value: 0 }, y: { value: 0 } }
84
356
 
85
357
  if (args.opener?.value && safeOpenerDomRef.value && openerRect?.value) {
86
- const leftWithoutXMargin =
87
- openerRect.value.left - parseFloat(elStyle.marginLeft) - parseFloat(elStyle.marginRight)
88
- const leftWithoutLeftMargin = openerRect.value.left - parseFloat(elStyle.marginLeft)
89
- const topWithoutYMargin =
90
- openerRect.value.top - parseFloat(elStyle.marginTop) - parseFloat(elStyle.marginBottom)
91
- const topWithoutTopMargin = openerRect.value.top - parseFloat(elStyle.marginTop)
92
-
93
- switch (actualPosition.value) {
94
- case 'auto':
95
- actualPosition.value = getHighestAvailableSpacePos(
96
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
97
- safeElementDomRef.value as HTMLElement,
98
- safeOpenerDomRef.value as HTMLElement
99
- )
100
- break
101
- case 'auto-vertical':
102
- actualPosition.value = getHighestAvailableSpacePos(
103
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
104
- safeElementDomRef.value as HTMLElement,
105
- safeOpenerDomRef.value as HTMLElement,
106
- undefined,
107
- true
108
- )
109
- break
110
- case 'auto-start':
111
- actualPosition.value = getHighestAvailableSpacePos(
112
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
113
- safeElementDomRef.value as HTMLElement,
114
- safeOpenerDomRef.value as HTMLElement,
115
- 'start'
116
- )
117
- break
118
- case 'auto-vertical-start':
119
- actualPosition.value = getHighestAvailableSpacePos(
120
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
121
- safeElementDomRef.value as HTMLElement,
122
- safeOpenerDomRef.value as HTMLElement,
123
- 'start',
124
- true
125
- )
126
- break
127
- case 'auto-end':
128
- actualPosition.value = getHighestAvailableSpacePos(
129
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
130
- safeElementDomRef.value as HTMLElement,
131
- safeOpenerDomRef.value as HTMLElement,
132
- 'end'
133
- )
134
- break
135
- case 'auto-vertical-end':
136
- actualPosition.value = getHighestAvailableSpacePos(
137
- args.useViewport?.value ? null : (safeContainerDomRef.value as HTMLElement),
138
- safeElementDomRef.value as HTMLElement,
139
- safeOpenerDomRef.value as HTMLElement,
140
- 'end',
141
- true
142
- )
143
- break
144
- default:
145
- break
146
- }
147
- switch (actualPosition.value) {
148
- case 'bottom':
149
- float.position.y = openerRect.value.bottom
150
- float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
151
- translateX = -50
152
- translateY = 0
153
- break
154
- case 'bottom-start':
155
- float.position.y = openerRect.value.bottom
156
- float.position.x = leftWithoutXMargin
157
- translateX = 0
158
- translateY = 0
159
- break
160
- case 'bottom-end':
161
- float.position.y = openerRect.value.bottom
162
- float.position.x = openerRect.value.right
163
- translateX = -100
164
- translateY = 0
165
- break
166
- case 'left-start':
167
- float.position.y = topWithoutYMargin
168
- float.position.x = leftWithoutXMargin
169
- translateX = -100
170
- translateY = 0
171
- break
172
- case 'left':
173
- float.position.y = topWithoutTopMargin + openerRect.value.height / 2
174
- float.position.x = leftWithoutXMargin
175
- translateY = -50
176
- translateX = -100
177
- break
178
- case 'left-end':
179
- float.position.y = openerRect.value.bottom
180
- float.position.x = leftWithoutXMargin
181
- translateY = -100
182
- translateX = -100
183
- break
184
- case 'top-start':
185
- float.position.y = topWithoutYMargin
186
- float.position.x = leftWithoutXMargin
187
- translateY = -100
188
- translateX = 0
189
- break
190
- case 'top':
191
- float.position.y = topWithoutYMargin
192
- float.position.x = leftWithoutLeftMargin + openerRect.value.width / 2
193
- translateX = -50
194
- translateY = -100
195
- break
196
- case 'top-end':
197
- float.position.y = topWithoutYMargin
198
- float.position.x = openerRect.value.right
199
- translateX = -100
200
- translateY = -100
201
- break
202
- case 'right-start':
203
- float.position.y = topWithoutYMargin
204
- float.position.x = openerRect.value.right
205
- translateX = 0
206
- translateY = 0
207
- break
208
- case 'right':
209
- float.position.y = topWithoutTopMargin + openerRect.value.height / 2
210
- float.position.x = openerRect.value.right
211
- translateY = -50
212
- translateX = 0
213
- break
214
- case 'right-end':
215
- float.position.y = openerRect.value.bottom
216
- float.position.x = openerRect.value.right
217
- translateY = -100
218
- translateX = 0
219
- break
220
- default:
221
- break
222
- }
358
+ calculatePositionWithOpener(elStyle, translate.x, translate.y)
223
359
  } else {
224
- switch (actualPosition.value) {
225
- case 'bottom':
226
- float.position.y = containerRect.value.bottom - rect.value.height
227
- float.position.x = containerRect.value.left + containerRect.value.width / 2
228
- translateX = -50
229
- break
230
- case 'left-end':
231
- case 'bottom-start':
232
- float.position.y = containerRect.value.bottom - rect.value.height
233
- float.position.x = containerRect.value.left
234
- break
235
- case 'right-end':
236
- case 'bottom-end':
237
- float.position.y = containerRect.value.bottom - rect.value.height
238
- float.position.x = containerRect.value.right - rect.value.width
239
- break
240
- case 'left':
241
- float.position.y =
242
- containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
243
- float.position.x = containerRect.value.left
244
- break
245
- case 'top-start':
246
- case 'left-start':
247
- float.position.y = containerRect.value.top
248
- float.position.x = containerRect.value.left
249
- break
250
- case 'top':
251
- float.position.y = containerRect.value.top
252
- float.position.x =
253
- containerRect.value.left + (containerRect.value.width - rect.value.width) / 2
254
- break
255
- case 'top-end':
256
- case 'right-start':
257
- float.position.y = containerRect.value.top
258
- float.position.x = containerRect.value.right - rect.value.width
259
- case 'right':
260
- float.position.y =
261
- containerRect.value.top + (containerRect.value.height - rect.value.height) / 2
262
- float.position.x = containerRect.value.right - rect.value.width
263
- break
264
- default:
265
- break
266
- }
267
- }
268
-
269
- const realPos = calcRealPos(rect.value, float.position, translateX, translateY)
270
-
271
- float.position.x = realPos.x
272
- float.position.y = realPos.y
273
-
274
- if (realPos.x < containerRect.value.left) {
275
- float.position.x = containerRect.value.left
276
- translateX = 0
277
- }
278
-
279
- if (realPos.x + rect.value.width > containerRect.value.right) {
280
- const fixVal = containerRect.value.right - rect.value.width
281
- if (fixVal > 0) {
282
- float.position.x = fixVal
283
- }
284
- translateX = 0
360
+ calculatePositionWithoutOpener(translate.x)
285
361
  }
286
362
 
287
- if (realPos.y < containerRect.value.top) {
288
- float.position.y = containerRect.value.top
289
- translateY = 0
290
- }
363
+ // Step 5: Calculate real position and apply boundary corrections
364
+ if (rect.value) {
365
+ const realPos = calcRealPos(
366
+ rect.value,
367
+ float.position,
368
+ translate.x.value,
369
+ translate.y.value
370
+ )
371
+ float.position.x = realPos.x
372
+ float.position.y = realPos.y
291
373
 
292
- if (realPos.y + rect.value.height > containerRect.value.bottom) {
293
- const fixVal = containerRect.value.bottom - rect.value.height
294
- if (fixVal > 0) {
295
- float.position.y = fixVal
296
- }
297
- translateY = 0
374
+ applyBoundaryCorrections(realPos, translate.x, translate.y)
298
375
  }
299
376
 
300
- safeElementDomRef.value.style.top = `${float.position.y}px`
301
- safeElementDomRef.value.style.left = `${float.position.x}px`
302
- safeElementDomRef.value.style.position = 'fixed'
303
- safeElementDomRef.value.style.display = 'flex'
377
+ // Step 6: Apply styles to element
378
+ applyElementStyles()
304
379
 
305
- rect.value = safeElementDomRef.value.getBoundingClientRect()
380
+ // Step 7: Update rect after positioning
381
+ rect.value = safeElementDomRef.value!.getBoundingClientRect()
306
382
 
383
+ // Step 8: Trigger callback if provided
307
384
  if (args.callback?.value && !ignoreCallback) {
308
385
  args.callback.value(rect, openerRect, containerRect, position, actualPosition)
309
386
  }
package/src/index.ts CHANGED
@@ -2,4 +2,4 @@ export { default as FzFloating } from './FzFloating.vue'
2
2
 
3
3
  export * from './composables'
4
4
  export * from './types'
5
- export * from './utils'
5
+ export * from './utils'
package/src/types.ts CHANGED
@@ -40,7 +40,7 @@ export interface FzFloatElement {
40
40
  export interface FzUseFloatingArgs {
41
41
  element: FzFloatElement
42
42
  container?: FzFloatElement
43
- opener: FzFloatElement
43
+ opener?: FzFloatElement
44
44
  position?: FzFloatingPosition
45
45
  useViewport?: boolean
46
46
  callback?: (