@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.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fiscozen/composables",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
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
9
  "dependencies": {
10
- "@fiscozen/style": "^0.1.7"
10
+ "@fiscozen/style": "^0.1.8-next.0"
11
11
  },
12
12
  "peerDependencies": {
13
13
  "tailwindcss": "^3.4.1",
@@ -29,8 +29,8 @@
29
29
  "vitest": "^1.2.0",
30
30
  "vue-tsc": "^1.8.25",
31
31
  "@fiscozen/tsconfig": "^0.1.0",
32
- "@fiscozen/prettier-config": "^0.1.0",
33
- "@fiscozen/eslint-config": "^0.1.0"
32
+ "@fiscozen/eslint-config": "^0.1.0",
33
+ "@fiscozen/prettier-config": "^0.1.0"
34
34
  },
35
35
  "license": "ISC",
36
36
  "scripts": {
@@ -76,6 +76,8 @@ watch(
76
76
  }
77
77
  window.addEventListener('scroll', setPositionWhenOpen)
78
78
  const openerRect = opener.value?.getBoundingClientRect()
79
+ // CRITICAL: Set position fixed immediately to prevent layout shift
80
+ content.value.style.position = 'fixed'
79
81
  content.value.style.top = '0px'
80
82
  content.value.style.left = '0px'
81
83
  content.value.style.transform = 'none'
@@ -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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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 z-10">
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
+ })