@fiscozen/composables 0.1.36 → 0.1.38-beta.0
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/package.json +6 -3
- package/src/FzFloating.vue +10 -2
- package/src/__tests__/FzFloating.spec.ts +15 -0
- package/src/__tests__/__snapshots__/FzFloating.spec.ts.snap +24 -24
- package/src/__tests__/useFloating.spec.ts +339 -0
- package/src/composables/useFloating.ts +320 -243
- package/src/composables/useMediaQuery.ts +2 -2
- package/src/index.ts +1 -1
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiscozen/composables",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38-beta.0",
|
|
4
4
|
"description": "Design System utility composables",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [],
|
|
8
8
|
"author": "Riccardo Agnoletto",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@fiscozen/style": "^0.1.8-next.0"
|
|
11
|
+
},
|
|
9
12
|
"peerDependencies": {
|
|
10
13
|
"tailwindcss": "^3.4.1",
|
|
11
14
|
"vue": "^3.4.13"
|
|
@@ -25,9 +28,9 @@
|
|
|
25
28
|
"vite": "^5.0.10",
|
|
26
29
|
"vitest": "^1.2.0",
|
|
27
30
|
"vue-tsc": "^1.8.25",
|
|
31
|
+
"@fiscozen/prettier-config": "^0.1.0",
|
|
28
32
|
"@fiscozen/eslint-config": "^0.1.0",
|
|
29
|
-
"@fiscozen/tsconfig": "^0.1.0"
|
|
30
|
-
"@fiscozen/prettier-config": "^0.1.0"
|
|
33
|
+
"@fiscozen/tsconfig": "^0.1.0"
|
|
31
34
|
},
|
|
32
35
|
"license": "ISC",
|
|
33
36
|
"scripts": {
|
package/src/FzFloating.vue
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { ref, useSlots, watch, toRef, computed, onBeforeUnmount, toRefs, defineExpose } from 'vue'
|
|
3
3
|
import { useFloating } from './composables'
|
|
4
4
|
import { FzFloatingProps, FzUseFloatingArgs } from './types'
|
|
5
|
+
import { useMediaQuery } from './composables'
|
|
6
|
+
import { breakpoints } from '@fiscozen/style'
|
|
5
7
|
|
|
6
8
|
const props = withDefaults(defineProps<FzFloatingProps>(), {
|
|
7
9
|
position: 'auto',
|
|
@@ -16,6 +18,8 @@ const opener = ref<HTMLElement | null>(null)
|
|
|
16
18
|
|
|
17
19
|
const slots = useSlots()
|
|
18
20
|
|
|
21
|
+
const xs = useMediaQuery(`(max-width: ${breakpoints.xs})`)
|
|
22
|
+
|
|
19
23
|
let scheduledAnimationFrame = false
|
|
20
24
|
|
|
21
25
|
const useFloatingOpts: FzUseFloatingArgs = {
|
|
@@ -71,9 +75,13 @@ watch(
|
|
|
71
75
|
return
|
|
72
76
|
}
|
|
73
77
|
window.addEventListener('scroll', setPositionWhenOpen)
|
|
78
|
+
const openerRect = opener.value?.getBoundingClientRect()
|
|
79
|
+
// CRITICAL: Set position fixed immediately to prevent layout shift
|
|
80
|
+
content.value.style.position = 'fixed'
|
|
74
81
|
content.value.style.top = '0px'
|
|
75
82
|
content.value.style.left = '0px'
|
|
76
83
|
content.value.style.transform = 'none'
|
|
84
|
+
content.value.style.width = xs.value ? openerRect?.width + 'px' : 'auto'
|
|
77
85
|
floating.setPosition()
|
|
78
86
|
}
|
|
79
87
|
)
|
|
@@ -110,7 +118,7 @@ defineExpose({
|
|
|
110
118
|
<template>
|
|
111
119
|
<div>
|
|
112
120
|
<slot name="opener-start"></slot>
|
|
113
|
-
<div ref="opener" class="inline-flex">
|
|
121
|
+
<div ref="opener" class="inline-flex w-full sm:w-auto">
|
|
114
122
|
<slot name="opener" :isOpen :floating></slot>
|
|
115
123
|
</div>
|
|
116
124
|
<slot name="opener-end"></slot>
|
|
@@ -118,7 +126,7 @@ defineExpose({
|
|
|
118
126
|
v-if="!teleport"
|
|
119
127
|
ref="content"
|
|
120
128
|
v-show="$slots.default && (!$slots.opener || ($slots.opener && isOpen))"
|
|
121
|
-
class="fz__floating__content"
|
|
129
|
+
class="fz__floating__content w-full sm:w-auto"
|
|
122
130
|
:class="contentClass"
|
|
123
131
|
>
|
|
124
132
|
<slot :isOpen :floating></slot>
|
|
@@ -26,6 +26,21 @@ beforeEach(() => {
|
|
|
26
26
|
disconnect: () => null
|
|
27
27
|
})
|
|
28
28
|
window.IntersectionObserver = mockIntersectionObserver
|
|
29
|
+
|
|
30
|
+
// Mock matchMedia for useMediaQuery composable
|
|
31
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
32
|
+
writable: true,
|
|
33
|
+
value: vi.fn().mockImplementation((query) => ({
|
|
34
|
+
matches: false,
|
|
35
|
+
media: query,
|
|
36
|
+
onchange: null,
|
|
37
|
+
addListener: vi.fn(),
|
|
38
|
+
removeListener: vi.fn(),
|
|
39
|
+
addEventListener: vi.fn(),
|
|
40
|
+
removeEventListener: vi.fn(),
|
|
41
|
+
dispatchEvent: vi.fn()
|
|
42
|
+
}))
|
|
43
|
+
})
|
|
29
44
|
})
|
|
30
45
|
describe('FzFloating', () => {
|
|
31
46
|
positions.forEach((pos) => {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`FzFloating > should match snapshot 1`] = `
|
|
4
4
|
"<div>
|
|
5
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
6
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
5
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
6
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
7
7
|
<div>content</div>
|
|
8
8
|
</div>
|
|
9
9
|
<!--v-if-->
|
|
@@ -12,8 +12,8 @@ exports[`FzFloating > should match snapshot 1`] = `
|
|
|
12
12
|
|
|
13
13
|
exports[`FzFloating > should match snapshot 2`] = `
|
|
14
14
|
"<div>
|
|
15
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
16
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
15
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
16
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
17
17
|
<div>content</div>
|
|
18
18
|
</div>
|
|
19
19
|
<!--v-if-->
|
|
@@ -22,8 +22,8 @@ exports[`FzFloating > should match snapshot 2`] = `
|
|
|
22
22
|
|
|
23
23
|
exports[`FzFloating > should match snapshot 3`] = `
|
|
24
24
|
"<div>
|
|
25
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
26
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
25
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
26
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
27
27
|
<div>content</div>
|
|
28
28
|
</div>
|
|
29
29
|
<!--v-if-->
|
|
@@ -32,8 +32,8 @@ exports[`FzFloating > should match snapshot 3`] = `
|
|
|
32
32
|
|
|
33
33
|
exports[`FzFloating > should match snapshot 4`] = `
|
|
34
34
|
"<div>
|
|
35
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
36
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
35
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
36
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
37
37
|
<div>content</div>
|
|
38
38
|
</div>
|
|
39
39
|
<!--v-if-->
|
|
@@ -42,8 +42,8 @@ exports[`FzFloating > should match snapshot 4`] = `
|
|
|
42
42
|
|
|
43
43
|
exports[`FzFloating > should match snapshot 5`] = `
|
|
44
44
|
"<div>
|
|
45
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
46
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
45
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
46
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
47
47
|
<div>content</div>
|
|
48
48
|
</div>
|
|
49
49
|
<!--v-if-->
|
|
@@ -52,8 +52,8 @@ exports[`FzFloating > should match snapshot 5`] = `
|
|
|
52
52
|
|
|
53
53
|
exports[`FzFloating > should match snapshot 6`] = `
|
|
54
54
|
"<div>
|
|
55
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
56
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
55
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
56
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
57
57
|
<div>content</div>
|
|
58
58
|
</div>
|
|
59
59
|
<!--v-if-->
|
|
@@ -62,8 +62,8 @@ exports[`FzFloating > should match snapshot 6`] = `
|
|
|
62
62
|
|
|
63
63
|
exports[`FzFloating > should match snapshot 7`] = `
|
|
64
64
|
"<div>
|
|
65
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
66
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
65
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
66
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
67
67
|
<div>content</div>
|
|
68
68
|
</div>
|
|
69
69
|
<!--v-if-->
|
|
@@ -72,8 +72,8 @@ exports[`FzFloating > should match snapshot 7`] = `
|
|
|
72
72
|
|
|
73
73
|
exports[`FzFloating > should match snapshot 8`] = `
|
|
74
74
|
"<div>
|
|
75
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
76
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
75
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
76
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
77
77
|
<div>content</div>
|
|
78
78
|
</div>
|
|
79
79
|
<!--v-if-->
|
|
@@ -82,8 +82,8 @@ exports[`FzFloating > should match snapshot 8`] = `
|
|
|
82
82
|
|
|
83
83
|
exports[`FzFloating > should match snapshot 9`] = `
|
|
84
84
|
"<div>
|
|
85
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
86
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
85
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
86
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
87
87
|
<div>content</div>
|
|
88
88
|
</div>
|
|
89
89
|
<!--v-if-->
|
|
@@ -92,8 +92,8 @@ exports[`FzFloating > should match snapshot 9`] = `
|
|
|
92
92
|
|
|
93
93
|
exports[`FzFloating > should match snapshot 10`] = `
|
|
94
94
|
"<div>
|
|
95
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
96
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
95
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
96
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
97
97
|
<div>content</div>
|
|
98
98
|
</div>
|
|
99
99
|
<!--v-if-->
|
|
@@ -102,8 +102,8 @@ exports[`FzFloating > should match snapshot 10`] = `
|
|
|
102
102
|
|
|
103
103
|
exports[`FzFloating > should match snapshot 11`] = `
|
|
104
104
|
"<div>
|
|
105
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
106
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
105
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
106
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
107
107
|
<div>content</div>
|
|
108
108
|
</div>
|
|
109
109
|
<!--v-if-->
|
|
@@ -112,8 +112,8 @@ exports[`FzFloating > should match snapshot 11`] = `
|
|
|
112
112
|
|
|
113
113
|
exports[`FzFloating > should match snapshot 12`] = `
|
|
114
114
|
"<div>
|
|
115
|
-
<div class="inline-flex"><button>opener</button></div>
|
|
116
|
-
<div class="fz__floating__content bg-core-white fixed p-4
|
|
115
|
+
<div class="inline-flex w-full sm:w-auto"><button>opener</button></div>
|
|
116
|
+
<div class="fz__floating__content w-full sm:w-auto bg-core-white fixed p-4">
|
|
117
117
|
<div>content</div>
|
|
118
118
|
</div>
|
|
119
119
|
<!--v-if-->
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
|
|
2
|
+
import { ref, toRefs, nextTick } from 'vue'
|
|
3
|
+
import { useFloating } from '../composables/useFloating'
|
|
4
|
+
import { FzFloatingPosition } from '../types'
|
|
5
|
+
|
|
6
|
+
describe('useFloating', () => {
|
|
7
|
+
let mockElement: HTMLElement
|
|
8
|
+
let mockOpener: HTMLElement
|
|
9
|
+
let mockContainer: HTMLElement
|
|
10
|
+
let mockIntersectionObserver: any
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Mock IntersectionObserver
|
|
14
|
+
mockIntersectionObserver = vi.fn()
|
|
15
|
+
mockIntersectionObserver.mockReturnValue({
|
|
16
|
+
observe: vi.fn(),
|
|
17
|
+
unobserve: vi.fn(),
|
|
18
|
+
disconnect: vi.fn()
|
|
19
|
+
})
|
|
20
|
+
window.IntersectionObserver = mockIntersectionObserver
|
|
21
|
+
|
|
22
|
+
// Mock DOM elements
|
|
23
|
+
mockElement = document.createElement('div')
|
|
24
|
+
mockElement.style.width = '200px'
|
|
25
|
+
mockElement.style.height = '100px'
|
|
26
|
+
document.body.appendChild(mockElement)
|
|
27
|
+
|
|
28
|
+
mockOpener = document.createElement('button')
|
|
29
|
+
mockOpener.style.width = '100px'
|
|
30
|
+
mockOpener.style.height = '40px'
|
|
31
|
+
document.body.appendChild(mockOpener)
|
|
32
|
+
|
|
33
|
+
mockContainer = document.body
|
|
34
|
+
|
|
35
|
+
// Mock getBoundingClientRect
|
|
36
|
+
mockElement.getBoundingClientRect = vi.fn(() => ({
|
|
37
|
+
width: 200,
|
|
38
|
+
height: 100,
|
|
39
|
+
top: 0,
|
|
40
|
+
left: 0,
|
|
41
|
+
right: 200,
|
|
42
|
+
bottom: 100,
|
|
43
|
+
x: 0,
|
|
44
|
+
y: 0,
|
|
45
|
+
toJSON: () => {}
|
|
46
|
+
})) as any
|
|
47
|
+
|
|
48
|
+
mockOpener.getBoundingClientRect = vi.fn(() => ({
|
|
49
|
+
width: 100,
|
|
50
|
+
height: 40,
|
|
51
|
+
top: 100,
|
|
52
|
+
left: 100,
|
|
53
|
+
right: 200,
|
|
54
|
+
bottom: 140,
|
|
55
|
+
x: 100,
|
|
56
|
+
y: 100,
|
|
57
|
+
toJSON: () => {}
|
|
58
|
+
})) as any
|
|
59
|
+
|
|
60
|
+
mockContainer.getBoundingClientRect = vi.fn(() => ({
|
|
61
|
+
width: 1024,
|
|
62
|
+
height: 768,
|
|
63
|
+
top: 0,
|
|
64
|
+
left: 0,
|
|
65
|
+
right: 1024,
|
|
66
|
+
bottom: 768,
|
|
67
|
+
x: 0,
|
|
68
|
+
y: 0,
|
|
69
|
+
toJSON: () => {}
|
|
70
|
+
})) as any
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
document.body.innerHTML = ''
|
|
75
|
+
vi.clearAllMocks()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('initialization', () => {
|
|
79
|
+
it('should initialize with correct default values', () => {
|
|
80
|
+
const args = toRefs({
|
|
81
|
+
position: ref<FzFloatingPosition>('auto'),
|
|
82
|
+
opener: { domRef: ref(mockOpener) },
|
|
83
|
+
element: { domRef: ref(mockElement) }
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const floating = useFloating(args)
|
|
87
|
+
|
|
88
|
+
expect(floating.float).toBeDefined()
|
|
89
|
+
expect(floating.rect).toBeDefined()
|
|
90
|
+
expect(floating.setPosition).toBeDefined()
|
|
91
|
+
expect(floating.position).toBeDefined()
|
|
92
|
+
expect(floating.actualPosition).toBeDefined()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should set up intersection observer', async () => {
|
|
96
|
+
const args = toRefs({
|
|
97
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
98
|
+
opener: { domRef: ref(mockOpener) },
|
|
99
|
+
element: { domRef: ref(mockElement) }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const floating = useFloating(args)
|
|
103
|
+
await floating.setPosition()
|
|
104
|
+
await nextTick()
|
|
105
|
+
|
|
106
|
+
expect(mockIntersectionObserver).toHaveBeenCalled()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('position calculation', () => {
|
|
111
|
+
const positions: FzFloatingPosition[] = [
|
|
112
|
+
'bottom',
|
|
113
|
+
'bottom-start',
|
|
114
|
+
'bottom-end',
|
|
115
|
+
'top',
|
|
116
|
+
'top-start',
|
|
117
|
+
'top-end',
|
|
118
|
+
'left',
|
|
119
|
+
'left-start',
|
|
120
|
+
'left-end',
|
|
121
|
+
'right',
|
|
122
|
+
'right-start',
|
|
123
|
+
'right-end'
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
positions.forEach((position) => {
|
|
127
|
+
it(`should calculate ${position} position correctly`, async () => {
|
|
128
|
+
const args = toRefs({
|
|
129
|
+
position: ref(position),
|
|
130
|
+
element: { domRef: ref(mockElement) },
|
|
131
|
+
opener: { domRef: ref(mockOpener) }
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const floating = useFloating(args)
|
|
135
|
+
await floating.setPosition()
|
|
136
|
+
await nextTick()
|
|
137
|
+
|
|
138
|
+
expect(floating.float.position).toBeDefined()
|
|
139
|
+
expect(typeof floating.float.position.x).toBe('number')
|
|
140
|
+
expect(typeof floating.float.position.y).toBe('number')
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe('auto positioning', () => {
|
|
146
|
+
it('should resolve auto position based on available space', async () => {
|
|
147
|
+
const args = toRefs({
|
|
148
|
+
position: ref<FzFloatingPosition>('auto'),
|
|
149
|
+
element: { domRef: ref(mockElement) },
|
|
150
|
+
opener: { domRef: ref(mockOpener) }
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
const floating = useFloating(args)
|
|
154
|
+
await floating.setPosition()
|
|
155
|
+
await nextTick()
|
|
156
|
+
|
|
157
|
+
expect(floating.actualPosition?.value).toBeDefined()
|
|
158
|
+
expect(floating.actualPosition?.value).not.toBe('auto')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should resolve auto-start position', async () => {
|
|
162
|
+
const args = toRefs({
|
|
163
|
+
position: ref<FzFloatingPosition>('auto-start'),
|
|
164
|
+
element: { domRef: ref(mockElement) },
|
|
165
|
+
opener: { domRef: ref(mockOpener) }
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const floating = useFloating(args)
|
|
169
|
+
await floating.setPosition()
|
|
170
|
+
await nextTick()
|
|
171
|
+
|
|
172
|
+
expect(floating.actualPosition?.value).toBeDefined()
|
|
173
|
+
expect(floating.actualPosition?.value).not.toBe('auto-start')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should resolve auto-end position', async () => {
|
|
177
|
+
const args = toRefs({
|
|
178
|
+
position: ref<FzFloatingPosition>('auto-end'),
|
|
179
|
+
element: { domRef: ref(mockElement) },
|
|
180
|
+
opener: { domRef: ref(mockOpener) }
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const floating = useFloating(args)
|
|
184
|
+
await floating.setPosition()
|
|
185
|
+
await nextTick()
|
|
186
|
+
|
|
187
|
+
expect(floating.actualPosition?.value).toBeDefined()
|
|
188
|
+
expect(floating.actualPosition?.value).not.toBe('auto-end')
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('boundary corrections', () => {
|
|
193
|
+
it('should keep element within container bounds', async () => {
|
|
194
|
+
// Position opener at edge of container
|
|
195
|
+
mockOpener.getBoundingClientRect = vi.fn(() => ({
|
|
196
|
+
width: 100,
|
|
197
|
+
height: 40,
|
|
198
|
+
top: 10,
|
|
199
|
+
left: 950,
|
|
200
|
+
right: 1050,
|
|
201
|
+
bottom: 50,
|
|
202
|
+
x: 950,
|
|
203
|
+
y: 10,
|
|
204
|
+
toJSON: () => {}
|
|
205
|
+
})) as any
|
|
206
|
+
|
|
207
|
+
const args = toRefs({
|
|
208
|
+
position: ref<FzFloatingPosition>('bottom-end'),
|
|
209
|
+
element: { domRef: ref(mockElement) },
|
|
210
|
+
opener: { domRef: ref(mockOpener) }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const floating = useFloating(args)
|
|
214
|
+
await floating.setPosition()
|
|
215
|
+
await nextTick()
|
|
216
|
+
|
|
217
|
+
// Element should be adjusted to fit within container
|
|
218
|
+
expect(floating.float.position.x).toBeLessThanOrEqual(1024)
|
|
219
|
+
expect(floating.float.position.y).toBeLessThanOrEqual(768)
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe('element styling', () => {
|
|
224
|
+
it('should apply position fixed immediately', async () => {
|
|
225
|
+
const args = toRefs({
|
|
226
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
227
|
+
element: { domRef: ref(mockElement) },
|
|
228
|
+
opener: { domRef: ref(mockOpener) }
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const floating = useFloating(args)
|
|
232
|
+
await floating.setPosition()
|
|
233
|
+
await nextTick()
|
|
234
|
+
|
|
235
|
+
expect(mockElement.style.position).toBe('fixed')
|
|
236
|
+
expect(mockElement.style.display).toBe('flex')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should update position styles', async () => {
|
|
240
|
+
const args = toRefs({
|
|
241
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
242
|
+
element: { domRef: ref(mockElement) },
|
|
243
|
+
opener: { domRef: ref(mockOpener) }
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const floating = useFloating(args)
|
|
247
|
+
await floating.setPosition()
|
|
248
|
+
await nextTick()
|
|
249
|
+
|
|
250
|
+
expect(mockElement.style.top).toBeTruthy()
|
|
251
|
+
expect(mockElement.style.left).toBeTruthy()
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('callback handling', () => {
|
|
256
|
+
it('should call callback when provided', async () => {
|
|
257
|
+
const callback = vi.fn()
|
|
258
|
+
const args = toRefs({
|
|
259
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
260
|
+
element: { domRef: ref(mockElement) },
|
|
261
|
+
opener: { domRef: ref(mockOpener) },
|
|
262
|
+
callback: ref(callback)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const floating = useFloating(args)
|
|
266
|
+
await floating.setPosition()
|
|
267
|
+
await nextTick()
|
|
268
|
+
|
|
269
|
+
expect(callback).toHaveBeenCalled()
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('should not call callback when ignoreCallback is true', async () => {
|
|
273
|
+
const callback = vi.fn()
|
|
274
|
+
const args = toRefs({
|
|
275
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
276
|
+
element: { domRef: ref(mockElement) },
|
|
277
|
+
opener: { domRef: ref(mockOpener) },
|
|
278
|
+
callback: ref(callback)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const floating = useFloating(args)
|
|
282
|
+
await floating.setPosition(true)
|
|
283
|
+
await nextTick()
|
|
284
|
+
|
|
285
|
+
expect(callback).not.toHaveBeenCalled()
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('without opener', () => {
|
|
290
|
+
it('should position element relative to container when no opener', async () => {
|
|
291
|
+
const args = toRefs({
|
|
292
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
293
|
+
opener: undefined,
|
|
294
|
+
element: { domRef: ref(mockElement) }
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const floating = useFloating(args)
|
|
298
|
+
await floating.setPosition()
|
|
299
|
+
await nextTick()
|
|
300
|
+
|
|
301
|
+
expect(floating.float.position).toBeDefined()
|
|
302
|
+
expect(typeof floating.float.position.x).toBe('number')
|
|
303
|
+
expect(typeof floating.float.position.y).toBe('number')
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('error handling', () => {
|
|
308
|
+
it('should throw error when element ref is missing', async () => {
|
|
309
|
+
const args = toRefs({
|
|
310
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
311
|
+
opener: { domRef: ref(mockOpener) },
|
|
312
|
+
element: { domRef: ref(null) }
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const floating = useFloating(args)
|
|
316
|
+
|
|
317
|
+
await expect(async () => {
|
|
318
|
+
await floating.setPosition()
|
|
319
|
+
await nextTick()
|
|
320
|
+
}).rejects.toThrow('missing reference element for floating behavior')
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
describe('cleanup', () => {
|
|
325
|
+
it('should disconnect intersection observer on unmount', () => {
|
|
326
|
+
const args = toRefs({
|
|
327
|
+
position: ref<FzFloatingPosition>('bottom'),
|
|
328
|
+
opener: { domRef: ref(mockOpener) },
|
|
329
|
+
element: { domRef: ref(mockElement) }
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
useFloating(args)
|
|
333
|
+
|
|
334
|
+
// Simulate component unmount
|
|
335
|
+
// Note: In a real scenario, this would be handled by Vue's lifecycle
|
|
336
|
+
expect(mockIntersectionObserver).toHaveBeenCalled()
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
})
|
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
|
343
|
+
containerRect.value = safeContainerDomRef.value!.getBoundingClientRect()
|
|
75
344
|
|
|
76
345
|
const elStyle = window.getComputedStyle(safeElementDomRef.value as HTMLElement)
|
|
77
346
|
|
|
78
|
-
//
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ref, onMounted, onUnmounted, ref } from 'vue'
|
|
1
|
+
import { Ref, onMounted, onUnmounted, ref, computed } from 'vue'
|
|
2
2
|
|
|
3
3
|
function useMediaQuery(mediaQuery: string): Ref<boolean> {
|
|
4
4
|
const mediaQueryList = window.matchMedia(mediaQuery)
|
|
@@ -16,7 +16,7 @@ function useMediaQuery(mediaQuery: string): Ref<boolean> {
|
|
|
16
16
|
mediaQueryList.removeEventListener('change', handleChange)
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
return matches
|
|
19
|
+
return computed(() => matches.value)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export { useMediaQuery }
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED