base-ui-vue 0.3.0 → 0.5.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.
Files changed (218) hide show
  1. package/dist/arrow/TooltipArrow.cjs +609 -0
  2. package/dist/arrow/TooltipArrow.cjs.map +1 -0
  3. package/dist/arrow/TooltipArrow.js +478 -0
  4. package/dist/arrow/TooltipArrow.js.map +1 -0
  5. package/dist/button/Button.cjs +5 -317
  6. package/dist/button/Button.cjs.map +1 -1
  7. package/dist/button/Button.js +3 -249
  8. package/dist/button/Button.js.map +1 -1
  9. package/dist/button/ToolbarButton.cjs +19 -6
  10. package/dist/button/ToolbarButton.cjs.map +1 -1
  11. package/dist/button/ToolbarButton.js +3 -2
  12. package/dist/button/ToolbarButton.js.map +1 -1
  13. package/dist/checkbox/index.cjs +20 -9
  14. package/dist/checkbox/index.cjs.map +1 -1
  15. package/dist/checkbox/index.js +7 -2
  16. package/dist/checkbox/index.js.map +1 -1
  17. package/dist/checkbox-group/CheckboxGroup.cjs +14 -13
  18. package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -1
  19. package/dist/checkbox-group/CheckboxGroup.js +2 -1
  20. package/dist/checkbox-group/CheckboxGroup.js.map +1 -1
  21. package/dist/components/FloatingPortal.cjs +66 -0
  22. package/dist/components/FloatingPortal.cjs.map +1 -0
  23. package/dist/components/FloatingPortal.js +55 -0
  24. package/dist/components/FloatingPortal.js.map +1 -0
  25. package/dist/components/FloatingRootStore.cjs +101 -0
  26. package/dist/components/FloatingRootStore.cjs.map +1 -0
  27. package/dist/components/FloatingRootStore.js +84 -0
  28. package/dist/components/FloatingRootStore.js.map +1 -0
  29. package/dist/components/FloatingTree.cjs +33 -0
  30. package/dist/components/FloatingTree.cjs.map +1 -0
  31. package/dist/components/FloatingTree.js +22 -0
  32. package/dist/components/FloatingTree.js.map +1 -0
  33. package/dist/composite/composite.cjs +6 -0
  34. package/dist/composite/composite.js +1 -1
  35. package/dist/content/ScrollAreaContent.cjs +3 -3
  36. package/dist/content/ScrollAreaContent.cjs.map +1 -1
  37. package/dist/content/ScrollAreaContent.js +1 -1
  38. package/dist/control/FieldControl.cjs +3 -3
  39. package/dist/control/FieldControl.cjs.map +1 -1
  40. package/dist/control/FieldControl.js +1 -1
  41. package/dist/control/SliderControl.cjs +3 -3
  42. package/dist/control/SliderControl.cjs.map +1 -1
  43. package/dist/control/SliderControl.js +3 -3
  44. package/dist/corner/ScrollAreaCorner.cjs +3 -3
  45. package/dist/corner/ScrollAreaCorner.cjs.map +1 -1
  46. package/dist/corner/ScrollAreaCorner.js +1 -1
  47. package/dist/decrement/NumberFieldDecrement.cjs +868 -0
  48. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  49. package/dist/decrement/NumberFieldDecrement.js +701 -0
  50. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  51. package/dist/description/FieldDescription.cjs +2 -2
  52. package/dist/description/FieldDescription.cjs.map +1 -1
  53. package/dist/description/FieldDescription.js +1 -1
  54. package/dist/error/FieldError.cjs +2 -2
  55. package/dist/error/FieldError.cjs.map +1 -1
  56. package/dist/error/FieldError.js +1 -1
  57. package/dist/fallback/AvatarFallback.cjs +4 -48
  58. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  59. package/dist/fallback/AvatarFallback.js +4 -42
  60. package/dist/fallback/AvatarFallback.js.map +1 -1
  61. package/dist/floating-ui-vue/index.js +638 -0
  62. package/dist/floating-ui-vue/index.js.map +1 -0
  63. package/dist/form/Form.cjs +3 -3
  64. package/dist/form/Form.cjs.map +1 -1
  65. package/dist/form/Form.js +1 -1
  66. package/dist/group/NumberFieldGroup.cjs +72 -0
  67. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  68. package/dist/group/NumberFieldGroup.js +67 -0
  69. package/dist/group/NumberFieldGroup.js.map +1 -0
  70. package/dist/group/ToolbarGroup.cjs +2 -2
  71. package/dist/group/ToolbarGroup.cjs.map +1 -1
  72. package/dist/group/ToolbarGroup.js +1 -1
  73. package/dist/header/AccordionHeader.cjs +2 -2
  74. package/dist/header/AccordionHeader.cjs.map +1 -1
  75. package/dist/header/AccordionHeader.js +1 -1
  76. package/dist/hooks/useDismiss.cjs +502 -0
  77. package/dist/hooks/useDismiss.cjs.map +1 -0
  78. package/dist/hooks/useHoverFloatingInteraction.cjs +233 -0
  79. package/dist/hooks/useHoverFloatingInteraction.cjs.map +1 -0
  80. package/dist/image/AvatarImage.cjs +4 -4
  81. package/dist/image/AvatarImage.cjs.map +1 -1
  82. package/dist/image/AvatarImage.js +1 -1
  83. package/dist/increment/NumberFieldIncrement.cjs +113 -0
  84. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  85. package/dist/increment/NumberFieldIncrement.js +108 -0
  86. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  87. package/dist/index.cjs +80 -8
  88. package/dist/index.d.cts +4328 -1600
  89. package/dist/index.d.cts.map +1 -1
  90. package/dist/index.d.ts +4328 -1600
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +8 -4
  93. package/dist/index2.cjs +9435 -3236
  94. package/dist/index2.cjs.map +1 -1
  95. package/dist/index2.js +8905 -3049
  96. package/dist/index2.js.map +1 -1
  97. package/dist/rolldown-runtime.js +36 -0
  98. package/package.json +1 -1
  99. package/src/floating-ui-vue/components/FloatingPortal.ts +171 -0
  100. package/src/floating-ui-vue/components/FloatingPortal.vue +259 -0
  101. package/src/floating-ui-vue/components/FloatingRootStore.ts +83 -0
  102. package/src/floating-ui-vue/components/FloatingTree.ts +64 -0
  103. package/src/floating-ui-vue/components/FloatingTreeStore.ts +21 -0
  104. package/src/floating-ui-vue/hooks/useDismiss.ts +689 -0
  105. package/src/floating-ui-vue/hooks/useHoverFloatingInteraction.ts +244 -0
  106. package/src/floating-ui-vue/hooks/useHoverInteractionSharedState.ts +125 -0
  107. package/src/floating-ui-vue/hooks/useHoverShared.ts +53 -0
  108. package/src/floating-ui-vue/index.ts +48 -0
  109. package/src/floating-ui-vue/middleware/arrow.ts +114 -0
  110. package/src/floating-ui-vue/types.ts +117 -0
  111. package/src/floating-ui-vue/utils/composite.ts +20 -0
  112. package/src/floating-ui-vue/utils/constants.ts +2 -0
  113. package/src/floating-ui-vue/utils/createAttribute.ts +3 -0
  114. package/src/floating-ui-vue/utils/createEventEmitter.ts +20 -0
  115. package/src/floating-ui-vue/utils/element.ts +59 -0
  116. package/src/floating-ui-vue/utils/event.ts +15 -0
  117. package/src/floating-ui-vue/utils/nodes.ts +14 -0
  118. package/src/floating-ui-vue/utils/tabbable.ts +309 -0
  119. package/src/index.ts +7 -1
  120. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  121. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  122. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  123. package/src/number-field/index.ts +42 -0
  124. package/src/number-field/input/NumberFieldInput.vue +455 -0
  125. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  126. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  127. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  128. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  129. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  130. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  131. package/src/number-field/utils/constants.ts +4 -0
  132. package/src/number-field/utils/getViewportRect.ts +34 -0
  133. package/src/number-field/utils/parse.ts +248 -0
  134. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  135. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  136. package/src/number-field/utils/types.ts +24 -0
  137. package/src/number-field/utils/validate.ts +120 -0
  138. package/src/otp-field/index.ts +22 -0
  139. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  140. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  141. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  142. package/src/otp-field/utils/otp.ts +135 -0
  143. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  144. package/src/radio/index.ts +28 -0
  145. package/src/radio/indicator/RadioIndicator.vue +90 -0
  146. package/src/radio/indicator/RadioIndicatorDataAttributes.ts +54 -0
  147. package/src/radio/root/RadioRoot.vue +544 -0
  148. package/src/radio/root/RadioRootContext.ts +18 -0
  149. package/src/radio/root/RadioRootDataAttributes.ts +46 -0
  150. package/src/radio/utils/stateAttributesMapping.ts +29 -0
  151. package/src/radio-group/RadioGroup.vue +381 -0
  152. package/src/radio-group/RadioGroupContext.ts +42 -0
  153. package/src/radio-group/RadioGroupDataAttributes.ts +6 -0
  154. package/src/radio-group/index.ts +10 -0
  155. package/src/tabs/index.ts +38 -0
  156. package/src/tabs/indicator/TabsIndicator.vue +201 -0
  157. package/src/tabs/indicator/TabsIndicatorCssVars.ts +26 -0
  158. package/src/tabs/indicator/TabsIndicatorDataAttributes.ts +12 -0
  159. package/src/tabs/indicator/prehydrationScript.min.ts +5 -0
  160. package/src/tabs/indicator/prehydrationScript.template.js +87 -0
  161. package/src/tabs/list/TabsList.vue +201 -0
  162. package/src/tabs/list/TabsListContext.ts +29 -0
  163. package/src/tabs/list/TabsListDataAttributes.ts +12 -0
  164. package/src/tabs/panel/TabsPanel.vue +167 -0
  165. package/src/tabs/panel/TabsPanelDataAttributes.ts +28 -0
  166. package/src/tabs/root/TabsRoot.vue +468 -0
  167. package/src/tabs/root/TabsRootContext.ts +56 -0
  168. package/src/tabs/root/TabsRootDataAttributes.ts +12 -0
  169. package/src/tabs/root/stateAttributesMapping.ts +9 -0
  170. package/src/tabs/tab/TabsTab.vue +274 -0
  171. package/src/tabs/tab/TabsTabDataAttributes.ts +20 -0
  172. package/src/tabs/utils/areTabValuesEqual.ts +6 -0
  173. package/src/tooltip/arrow/TooltipArrow.vue +116 -0
  174. package/src/tooltip/arrow/TooltipArrowDataAttributes.ts +31 -0
  175. package/src/tooltip/index.ts +54 -0
  176. package/src/tooltip/popup/TooltipPopup.vue +142 -0
  177. package/src/tooltip/popup/TooltipPopupDataAttributes.ts +35 -0
  178. package/src/tooltip/portal/TooltipPortal.vue +83 -0
  179. package/src/tooltip/portal/TooltipPortalContext.ts +19 -0
  180. package/src/tooltip/positioner/TooltipPositioner.vue +221 -0
  181. package/src/tooltip/positioner/TooltipPositionerContext.ts +23 -0
  182. package/src/tooltip/positioner/TooltipPositionerCssVars.ts +27 -0
  183. package/src/tooltip/positioner/TooltipPositionerDataAttributes.ts +26 -0
  184. package/src/tooltip/provider/TooltipProvider.vue +78 -0
  185. package/src/tooltip/provider/TooltipProviderContext.ts +19 -0
  186. package/src/tooltip/root/TooltipRoot.vue +401 -0
  187. package/src/tooltip/root/TooltipRootContext.ts +71 -0
  188. package/src/tooltip/store/TooltipHandle.ts +215 -0
  189. package/src/tooltip/trigger/TooltipTrigger.vue +343 -0
  190. package/src/tooltip/trigger/TooltipTriggerDataAttributes.ts +12 -0
  191. package/src/tooltip/utils/constants.ts +2 -0
  192. package/src/tooltip/viewport/TooltipViewport.vue +149 -0
  193. package/src/tooltip/viewport/TooltipViewportCssVars.ts +14 -0
  194. package/src/tooltip/viewport/TooltipViewportDataAttributes.ts +26 -0
  195. package/src/utils/FloatingPortalLite.vue +66 -0
  196. package/src/utils/FocusGuard.vue +36 -0
  197. package/src/utils/adaptiveOriginMiddleware.ts +79 -0
  198. package/src/utils/addEventListener.ts +16 -0
  199. package/src/utils/constants.ts +11 -0
  200. package/src/utils/createBaseUIEventDetails.ts +2 -0
  201. package/src/utils/detectBrowser.ts +15 -0
  202. package/src/utils/formatNumber.ts +35 -2
  203. package/src/utils/getCssDimensions.ts +19 -0
  204. package/src/utils/getDisabledMountTransitionStyles.ts +9 -0
  205. package/src/utils/hideMiddleware.ts +19 -0
  206. package/src/utils/mergeCleanups.ts +7 -0
  207. package/src/utils/platform.ts +18 -0
  208. package/src/utils/popupStateMapping.ts +103 -0
  209. package/src/utils/reasons.ts +4 -0
  210. package/src/utils/serializeValue.ts +17 -0
  211. package/src/utils/useAnchorPositioning.ts +644 -0
  212. package/src/utils/useInterval.ts +45 -0
  213. package/src/utils/usePopupAutoResize.ts +254 -0
  214. package/src/utils/usePopupViewport.ts +398 -0
  215. package/src/utils/usePositioner.ts +61 -0
  216. package/src/utils/usePressAndHold.ts +260 -0
  217. package/src/utils/useRenderElement.ts +3 -0
  218. package/src/utils/useValueChanged.ts +21 -0
@@ -0,0 +1,254 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import type { TextDirection } from '../direction-provider/DirectionContext'
3
+ import type { Dimensions } from '../floating-ui-vue/types'
4
+ import type { Side } from './useAnchorPositioning'
5
+ import { computed, nextTick, onBeforeUnmount, shallowRef, toValue, watch } from 'vue'
6
+ import { EMPTY_OBJECT } from './constants'
7
+ import { NOOP } from './empty'
8
+ import { getCssDimensions } from './getCssDimensions'
9
+ import { useAnimationFrame } from './useAnimationFrame'
10
+ import { useAnimationsFinished } from './useAnimationsFinished'
11
+
12
+ /**
13
+ * Allows the popup to resize based on changed content while preserving CSS transitions.
14
+ */
15
+ export function usePopupAutoResize(parameters: UsePopupAutoResizeParameters) {
16
+ const popupElement = computed(() => toValue(parameters.popupElement) ?? null)
17
+ const positionerElement = computed(() => toValue(parameters.positionerElement) ?? null)
18
+ const mounted = computed(() => toValue(parameters.mounted))
19
+ const content = computed(() => toValue(parameters.content))
20
+ const side = computed(() => toValue(parameters.side) ?? 'top')
21
+ const direction = computed(() => toValue(parameters.direction) ?? 'ltr')
22
+
23
+ const runOnceAnimationsFinish = useAnimationsFinished(popupElement, true, false)
24
+ const animationFrame = useAnimationFrame()
25
+ const committedDimensions = shallowRef<Dimensions | null>(null)
26
+ const isInitialRender = shallowRef(true)
27
+
28
+ let restoreAnchoringStyles = NOOP
29
+
30
+ const anchoringStyles = computed(() => getAnchoringStyles(side.value, direction.value))
31
+
32
+ watch(
33
+ [
34
+ mounted,
35
+ popupElement,
36
+ positionerElement,
37
+ content,
38
+ anchoringStyles,
39
+ ],
40
+ ([isMounted, popup, positioner], _previous, onCleanup) => {
41
+ if (!isMounted) {
42
+ restoreAnchoringStyles()
43
+ restoreAnchoringStyles = NOOP
44
+ isInitialRender.value = true
45
+ committedDimensions.value = null
46
+ return
47
+ }
48
+
49
+ if (!popup || !positioner) {
50
+ return
51
+ }
52
+
53
+ restoreAnchoringStyles = applyElementStyles(popup, anchoringStyles.value)
54
+
55
+ setPopupCssSize(popup, 'auto')
56
+
57
+ const restorePopupPosition = overrideElementStyle(popup, 'position', 'static')
58
+ const restorePopupTransform = overrideElementStyle(popup, 'transform', 'none')
59
+ const restorePopupScale = overrideElementStyle(popup, 'scale', '1')
60
+ const restorePositionerAvailableSize = applyElementStyles(positioner, {
61
+ '--available-width': 'max-content',
62
+ '--available-height': 'max-content',
63
+ })
64
+
65
+ function restoreMeasurementOverrides() {
66
+ restorePopupPosition()
67
+ restorePopupTransform()
68
+ restorePositionerAvailableSize()
69
+ }
70
+
71
+ function restoreMeasurementOverridesIncludingScale() {
72
+ restoreMeasurementOverrides()
73
+ restorePopupScale()
74
+ }
75
+
76
+ parameters.onMeasureLayout?.()
77
+
78
+ if (isInitialRender.value || committedDimensions.value == null) {
79
+ setPositionerCssSize(positioner, 'max-content')
80
+
81
+ const dimensions = getCssDimensions(popup)
82
+
83
+ committedDimensions.value = dimensions
84
+
85
+ commitPositionerCssSize(positioner, dimensions)
86
+ restoreMeasurementOverridesIncludingScale()
87
+ parameters.onMeasureLayoutComplete?.(null, dimensions)
88
+
89
+ isInitialRender.value = false
90
+
91
+ onCleanup(() => {
92
+ restoreAnchoringStyles()
93
+ restoreAnchoringStyles = NOOP
94
+ })
95
+
96
+ return
97
+ }
98
+
99
+ setPositionerCssSize(positioner, 'max-content')
100
+
101
+ const previousDimensions = committedDimensions.value
102
+ const newDimensions = getCssDimensions(popup)
103
+
104
+ committedDimensions.value = newDimensions
105
+
106
+ setPopupCssSize(popup, previousDimensions)
107
+ restoreMeasurementOverridesIncludingScale()
108
+ parameters.onMeasureLayoutComplete?.(previousDimensions, newDimensions)
109
+
110
+ commitPositionerCssSize(positioner, newDimensions)
111
+
112
+ const abortController = new AbortController()
113
+
114
+ animationFrame.request(() => {
115
+ setPopupCssSize(popup, newDimensions)
116
+
117
+ runOnceAnimationsFinish(() => {
118
+ popup.style.setProperty('--popup-width', 'auto')
119
+ popup.style.setProperty('--popup-height', 'auto')
120
+ }, abortController.signal)
121
+ })
122
+
123
+ onCleanup(() => {
124
+ abortController.abort()
125
+ animationFrame.cancel()
126
+ restoreAnchoringStyles()
127
+ restoreAnchoringStyles = NOOP
128
+ })
129
+ },
130
+ { flush: 'post', immediate: true },
131
+ )
132
+
133
+ onBeforeUnmount(() => {
134
+ animationFrame.cancel()
135
+ restoreAnchoringStyles()
136
+ restoreAnchoringStyles = NOOP
137
+ })
138
+ }
139
+
140
+ interface UsePopupAutoResizeParameters {
141
+ /**
142
+ * Element to resize.
143
+ */
144
+ popupElement: MaybeRefOrGetter<HTMLElement | null | undefined>
145
+ /**
146
+ * Positioner element, the parent of the popup.
147
+ */
148
+ positionerElement: MaybeRefOrGetter<HTMLElement | null | undefined>
149
+ /**
150
+ * Whether the popup is mounted.
151
+ */
152
+ mounted: MaybeRefOrGetter<boolean>
153
+ /**
154
+ * Content value that should trigger a resize.
155
+ */
156
+ content: MaybeRefOrGetter<unknown>
157
+ /**
158
+ * Callback fired before measuring the new layout.
159
+ */
160
+ onMeasureLayout?: (() => void) | undefined
161
+ /**
162
+ * Callback fired after the new dimensions have been measured.
163
+ */
164
+ onMeasureLayoutComplete?:
165
+ | ((previousDimensions: Dimensions | null, newDimensions: Dimensions) => void)
166
+ | undefined
167
+ /**
168
+ * Current logical side of the popup.
169
+ */
170
+ side: MaybeRefOrGetter<Side | undefined>
171
+ /**
172
+ * Current text direction.
173
+ */
174
+ direction: MaybeRefOrGetter<TextDirection>
175
+ }
176
+
177
+ function getAnchoringStyles(side: Side, direction: TextDirection): Record<string, string> {
178
+ let isOriginSide = side === 'top'
179
+ let isPhysicalLeft = side === 'left'
180
+
181
+ if (direction === 'rtl') {
182
+ isOriginSide = isOriginSide || side === 'inline-end'
183
+ isPhysicalLeft = isPhysicalLeft || side === 'inline-end'
184
+ }
185
+ else {
186
+ isOriginSide = isOriginSide || side === 'inline-start'
187
+ isPhysicalLeft = isPhysicalLeft || side === 'inline-start'
188
+ }
189
+
190
+ if (!isOriginSide) {
191
+ return EMPTY_OBJECT as Record<string, string>
192
+ }
193
+
194
+ return {
195
+ position: 'absolute',
196
+ [side === 'top' ? 'bottom' : 'top']: '0',
197
+ [isPhysicalLeft ? 'right' : 'left']: '0',
198
+ }
199
+ }
200
+
201
+ function overrideElementStyle(element: HTMLElement, property: string, value: string) {
202
+ const originalValue = element.style.getPropertyValue(property)
203
+
204
+ element.style.setProperty(property, value)
205
+
206
+ return () => {
207
+ if (originalValue) {
208
+ element.style.setProperty(property, originalValue)
209
+ }
210
+ else {
211
+ element.style.removeProperty(property)
212
+ }
213
+ }
214
+ }
215
+
216
+ function applyElementStyles(element: HTMLElement, styles: Record<string, string>) {
217
+ const restorers: Array<() => void> = []
218
+
219
+ for (const [key, value] of Object.entries(styles)) {
220
+ restorers.push(overrideElementStyle(element, key, value))
221
+ }
222
+
223
+ return restorers.length
224
+ ? () => {
225
+ restorers.forEach(restore => restore())
226
+ }
227
+ : NOOP
228
+ }
229
+
230
+ function setPopupCssSize(popupElement: HTMLElement, size: Dimensions | 'auto') {
231
+ const width = size === 'auto' ? 'auto' : `${size.width}px`
232
+ const height = size === 'auto' ? 'auto' : `${size.height}px`
233
+
234
+ popupElement.style.setProperty('--popup-width', width)
235
+ popupElement.style.setProperty('--popup-height', height)
236
+ }
237
+
238
+ function setPositionerCssSize(positionerElement: HTMLElement, size: Dimensions | 'max-content') {
239
+ const width = size === 'max-content' ? 'max-content' : `${size.width}px`
240
+ const height = size === 'max-content' ? 'max-content' : `${size.height}px`
241
+
242
+ positionerElement.style.setProperty('--positioner-width', width)
243
+ positionerElement.style.setProperty('--positioner-height', height)
244
+ }
245
+
246
+ function commitPositionerCssSize(positionerElement: HTMLElement, size: Dimensions) {
247
+ setPositionerCssSize(positionerElement, size)
248
+
249
+ nextTick(() => {
250
+ if (positionerElement.isConnected) {
251
+ setPositionerCssSize(positionerElement, size)
252
+ }
253
+ })
254
+ }
@@ -0,0 +1,398 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, ShallowRef } from 'vue'
2
+ import type { TextDirection } from '../direction-provider/DirectionContext'
3
+ import type { Dimensions } from '../floating-ui-vue/types'
4
+ import type { Side } from './useAnchorPositioning'
5
+ import { computed, nextTick, onBeforeUnmount, shallowRef, toValue, watch } from 'vue'
6
+ import { TransitionStatusDataAttributes } from './stateAttributesMapping'
7
+ import { useAnimationFrame } from './useAnimationFrame'
8
+ import { useAnimationsFinished } from './useAnimationsFinished'
9
+ import { usePopupAutoResize } from './usePopupAutoResize'
10
+
11
+ export interface PopupViewportCssVars {
12
+ /**
13
+ * CSS variable name storing the popup width for the previous content snapshot.
14
+ */
15
+ popupWidth: string
16
+ /**
17
+ * CSS variable name storing the popup height for the previous content snapshot.
18
+ */
19
+ popupHeight: string
20
+ }
21
+
22
+ export interface PopupViewportState {
23
+ /**
24
+ * Direction from which the popup was activated, used for directional animations.
25
+ */
26
+ activationDirection: string | undefined
27
+ /**
28
+ * Whether the viewport is currently transitioning between contents.
29
+ */
30
+ transitioning: boolean
31
+ }
32
+
33
+ export interface UsePopupViewportParameters {
34
+ /**
35
+ * The trigger currently associated with the open popup.
36
+ */
37
+ activeTriggerElement: MaybeRefOrGetter<Element | null | undefined>
38
+ /**
39
+ * ID of the active trigger.
40
+ */
41
+ activeTriggerId: MaybeRefOrGetter<string | null | undefined>
42
+ /**
43
+ * Whether the popup is open.
44
+ */
45
+ open: MaybeRefOrGetter<boolean>
46
+ /**
47
+ * Whether the popup is mounted.
48
+ */
49
+ mounted: MaybeRefOrGetter<boolean>
50
+ /**
51
+ * Element to resize.
52
+ */
53
+ popupElement: MaybeRefOrGetter<HTMLElement | null | undefined>
54
+ /**
55
+ * Positioner element, the parent of the popup.
56
+ */
57
+ positionerElement: MaybeRefOrGetter<HTMLElement | null | undefined>
58
+ /**
59
+ * Current logical side of the popup.
60
+ */
61
+ side: MaybeRefOrGetter<Side | undefined>
62
+ /**
63
+ * Current text direction.
64
+ */
65
+ direction: MaybeRefOrGetter<TextDirection>
66
+ /**
67
+ * Trigger payload used to render the current content.
68
+ */
69
+ payload: MaybeRefOrGetter<unknown>
70
+ /**
71
+ * CSS variable names used for sizing the previous content snapshot.
72
+ */
73
+ cssVars: PopupViewportCssVars
74
+ }
75
+
76
+ export interface UsePopupViewportResult {
77
+ /**
78
+ * Ref for the current content container.
79
+ */
80
+ currentContainerRef: ShallowRef<HTMLDivElement | null>
81
+ /**
82
+ * Ref for the previous content snapshot container.
83
+ */
84
+ previousContainerRef: ShallowRef<HTMLDivElement | null>
85
+ /**
86
+ * Key that remounts current content when triggers or delayed payloads change.
87
+ */
88
+ currentContentKey: ComputedRef<string>
89
+ /**
90
+ * Props for the current content container.
91
+ */
92
+ currentContainerProps: ComputedRef<Record<string, unknown>>
93
+ /**
94
+ * Props for the previous content snapshot container.
95
+ */
96
+ previousContainerProps: ComputedRef<Record<string, unknown>>
97
+ /**
98
+ * Whether the previous content snapshot should be rendered.
99
+ */
100
+ transitioning: ComputedRef<boolean>
101
+ /**
102
+ * Viewport state used for data attributes and render prop styling.
103
+ */
104
+ state: ComputedRef<PopupViewportState>
105
+ }
106
+
107
+ /**
108
+ * Builds morphing viewport containers for popups that animate between trigger-based content.
109
+ * Vue renders the new content normally and keeps a cloned DOM snapshot of the previous content
110
+ * around until the current container's animations finish.
111
+ */
112
+ export function usePopupViewport(parameters: UsePopupViewportParameters): UsePopupViewportResult {
113
+ const activeTriggerElement = computed(() => toValue(parameters.activeTriggerElement) ?? null)
114
+ const activeTriggerId = computed(() => toValue(parameters.activeTriggerId) ?? null)
115
+ const open = computed(() => toValue(parameters.open))
116
+ const mounted = computed(() => toValue(parameters.mounted))
117
+ const payload = computed(() => toValue(parameters.payload))
118
+
119
+ const currentContainerRef = shallowRef<HTMLDivElement | null>(null)
120
+ const previousContainerRef = shallowRef<HTMLDivElement | null>(null)
121
+ const previousContentNode = shallowRef<HTMLDivElement | null>(null)
122
+ const previousContentDimensions = shallowRef<{ width: number, height: number } | null>(null)
123
+ const newTriggerOffset = shallowRef<Offset | null>(null)
124
+ const showStartingStyleAttribute = shallowRef(false)
125
+ const lastHandledTrigger = shallowRef<Element | null>(null)
126
+
127
+ const cleanupFrame = useAnimationFrame()
128
+ const onAnimationsFinished = useAnimationsFinished(currentContainerRef, true, false)
129
+ let animationAbortController: AbortController | null = null
130
+
131
+ const currentContentKey = usePopupContentKey(activeTriggerId, payload)
132
+
133
+ function abortPendingAnimationCleanup() {
134
+ animationAbortController?.abort()
135
+ animationAbortController = null
136
+ }
137
+
138
+ function clearPreviousContent() {
139
+ previousContentNode.value = null
140
+ previousContentDimensions.value = null
141
+ }
142
+
143
+ watch(
144
+ activeTriggerElement,
145
+ (nextTrigger, previousTrigger) => {
146
+ if (
147
+ !open.value
148
+ || !nextTrigger
149
+ || !previousTrigger
150
+ || nextTrigger === previousTrigger
151
+ || lastHandledTrigger.value === nextTrigger
152
+ || !currentContainerRef.value
153
+ ) {
154
+ return
155
+ }
156
+
157
+ abortPendingAnimationCleanup()
158
+
159
+ previousContentNode.value = cloneContent(currentContainerRef.value)
160
+ previousContentDimensions.value = measureElement(currentContainerRef.value)
161
+ showStartingStyleAttribute.value = true
162
+ newTriggerOffset.value = calculateRelativePosition(previousTrigger, nextTrigger)
163
+ const currentAnimationAbortController = new AbortController()
164
+ animationAbortController = currentAnimationAbortController
165
+
166
+ cleanupFrame.request(() => {
167
+ showStartingStyleAttribute.value = false
168
+
169
+ nextTick(() => {
170
+ onAnimationsFinished(() => {
171
+ if (currentAnimationAbortController.signal.aborted) {
172
+ return
173
+ }
174
+
175
+ if (animationAbortController === currentAnimationAbortController) {
176
+ animationAbortController = null
177
+ }
178
+
179
+ clearPreviousContent()
180
+ }, currentAnimationAbortController.signal)
181
+ })
182
+ })
183
+
184
+ lastHandledTrigger.value = nextTrigger
185
+ },
186
+ { flush: 'pre' },
187
+ )
188
+
189
+ watch(open, (isOpen) => {
190
+ if (!isOpen) {
191
+ abortPendingAnimationCleanup()
192
+ clearPreviousContent()
193
+ newTriggerOffset.value = null
194
+ lastHandledTrigger.value = null
195
+ }
196
+ })
197
+
198
+ watch(
199
+ [previousContainerRef, previousContentNode],
200
+ async ([container, previousNode]) => {
201
+ if (!container || !previousNode) {
202
+ return
203
+ }
204
+
205
+ await nextTick()
206
+
207
+ container.replaceChildren(
208
+ ...Array.from(previousNode.childNodes).map(node => node.cloneNode(true)),
209
+ )
210
+ },
211
+ { flush: 'post' },
212
+ )
213
+
214
+ onBeforeUnmount(() => {
215
+ abortPendingAnimationCleanup()
216
+ cleanupFrame.cancel()
217
+ })
218
+
219
+ function handleMeasureLayout() {
220
+ currentContainerRef.value?.style.setProperty('animation', 'none')
221
+ currentContainerRef.value?.style.setProperty('transition', 'none')
222
+
223
+ previousContainerRef.value?.style.setProperty('display', 'none')
224
+ }
225
+
226
+ function handleMeasureLayoutComplete(previousDimensions: Dimensions | null) {
227
+ currentContainerRef.value?.style.removeProperty('animation')
228
+ currentContainerRef.value?.style.removeProperty('transition')
229
+
230
+ previousContainerRef.value?.style.removeProperty('display')
231
+
232
+ if (previousDimensions && (previousDimensions.width > 0 || previousDimensions.height > 0)) {
233
+ previousContentDimensions.value = previousDimensions
234
+ }
235
+ }
236
+
237
+ usePopupAutoResize({
238
+ popupElement: parameters.popupElement,
239
+ positionerElement: parameters.positionerElement,
240
+ mounted,
241
+ content: payload,
242
+ onMeasureLayout: handleMeasureLayout,
243
+ onMeasureLayoutComplete: handleMeasureLayoutComplete,
244
+ side: parameters.side,
245
+ direction: parameters.direction,
246
+ })
247
+
248
+ const transitioning = computed(() => previousContentNode.value != null)
249
+
250
+ const currentContainerProps = computed(() => ({
251
+ 'data-current': '',
252
+ [TransitionStatusDataAttributes.startingStyle]: showStartingStyleAttribute.value
253
+ ? ''
254
+ : undefined,
255
+ }))
256
+
257
+ const previousContainerProps = computed(() => ({
258
+ 'data-previous': '',
259
+ 'inert': '',
260
+ [TransitionStatusDataAttributes.endingStyle]: showStartingStyleAttribute.value
261
+ ? undefined
262
+ : '',
263
+ 'style': {
264
+ ...(previousContentDimensions.value
265
+ ? {
266
+ [parameters.cssVars.popupWidth]: `${previousContentDimensions.value.width}px`,
267
+ [parameters.cssVars.popupHeight]: `${previousContentDimensions.value.height}px`,
268
+ }
269
+ : null),
270
+ position: 'absolute',
271
+ },
272
+ }))
273
+
274
+ const state = computed<PopupViewportState>(() => ({
275
+ activationDirection: getActivationDirection(newTriggerOffset.value),
276
+ transitioning: transitioning.value,
277
+ }))
278
+
279
+ return {
280
+ currentContainerRef,
281
+ previousContainerRef,
282
+ currentContentKey,
283
+ currentContainerProps,
284
+ previousContainerProps,
285
+ transitioning,
286
+ state,
287
+ }
288
+ }
289
+
290
+ interface Offset {
291
+ horizontal: number
292
+ vertical: number
293
+ }
294
+
295
+ function cloneContent(source: HTMLElement) {
296
+ const wrapper = source.ownerDocument.createElement('div')
297
+
298
+ for (const child of Array.from(source.childNodes)) {
299
+ wrapper.appendChild(child.cloneNode(true))
300
+ }
301
+
302
+ return wrapper
303
+ }
304
+
305
+ function measureElement(element: HTMLElement) {
306
+ const rect = element.getBoundingClientRect()
307
+
308
+ return {
309
+ width: rect.width,
310
+ height: rect.height,
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Returns a string describing both the horizontal and vertical offset.
316
+ */
317
+ function getActivationDirection(offset: Offset | null): string | undefined {
318
+ if (!offset) {
319
+ return undefined
320
+ }
321
+
322
+ return `${getValueWithTolerance(offset.horizontal, 5, 'right', 'left')} ${getValueWithTolerance(offset.vertical, 5, 'down', 'up')}`
323
+ }
324
+
325
+ function getValueWithTolerance(
326
+ value: number,
327
+ tolerance: number,
328
+ positiveLabel: string,
329
+ negativeLabel: string,
330
+ ) {
331
+ if (value > tolerance) {
332
+ return positiveLabel
333
+ }
334
+
335
+ if (value < -tolerance) {
336
+ return negativeLabel
337
+ }
338
+
339
+ return ''
340
+ }
341
+
342
+ /**
343
+ * Calculates the relative position between centers of two elements.
344
+ */
345
+ function calculateRelativePosition(from: Element, to: Element): Offset {
346
+ const fromRect = from.getBoundingClientRect()
347
+ const toRect = to.getBoundingClientRect()
348
+
349
+ const fromCenter = {
350
+ x: fromRect.left + fromRect.width / 2,
351
+ y: fromRect.top + fromRect.height / 2,
352
+ }
353
+ const toCenter = {
354
+ x: toRect.left + toRect.width / 2,
355
+ y: toRect.top + toRect.height / 2,
356
+ }
357
+
358
+ return {
359
+ horizontal: toCenter.x - fromCenter.x,
360
+ vertical: toCenter.y - fromCenter.y,
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Returns a key that forces remounting content when triggers change or a payload is updated.
366
+ */
367
+ function usePopupContentKey(
368
+ activeTriggerId: ComputedRef<string | null>,
369
+ payload: ComputedRef<unknown>,
370
+ ): ComputedRef<string> {
371
+ const contentKey = shallowRef(0)
372
+ const previousActiveTriggerId = shallowRef(activeTriggerId.value)
373
+ const previousPayload = shallowRef(payload.value)
374
+ const pendingPayloadUpdate = shallowRef(false)
375
+
376
+ watch(
377
+ [activeTriggerId, payload],
378
+ ([nextActiveTriggerId, nextPayload]) => {
379
+ const triggerIdChanged = nextActiveTriggerId !== previousActiveTriggerId.value
380
+ const payloadChanged = nextPayload !== previousPayload.value
381
+
382
+ if (triggerIdChanged) {
383
+ contentKey.value += 1
384
+ pendingPayloadUpdate.value = !payloadChanged
385
+ }
386
+ else if (pendingPayloadUpdate.value && payloadChanged) {
387
+ contentKey.value += 1
388
+ pendingPayloadUpdate.value = false
389
+ }
390
+
391
+ previousActiveTriggerId.value = nextActiveTriggerId
392
+ previousPayload.value = nextPayload
393
+ },
394
+ { flush: 'pre' },
395
+ )
396
+
397
+ return computed(() => `${activeTriggerId.value ?? 'current'}-${contentKey.value}`)
398
+ }
@@ -0,0 +1,61 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, Ref, StyleValue } from 'vue'
2
+ import type { RenderRef, UseRenderElementComponentProps } from './useRenderElement'
3
+ import type { TransitionStatus } from './useTransitionStatus'
4
+ import { computed, toValue } from 'vue'
5
+ import { mergeProps } from '../merge-props/mergeProps'
6
+ import { getDisabledMountTransitionStyles } from './getDisabledMountTransitionStyles'
7
+ import { popupStateMapping } from './popupStateMapping'
8
+ import { useRenderElement } from './useRenderElement'
9
+
10
+ interface UsePositionerOptions {
11
+ styles: MaybeRefOrGetter<StyleValue>
12
+ transitionStatus: MaybeRefOrGetter<TransitionStatus>
13
+ props?: MaybeRefOrGetter<Record<string, any> | undefined>
14
+ refs?: RenderRef | undefined
15
+ hidden?: MaybeRefOrGetter<boolean | undefined>
16
+ inert?: MaybeRefOrGetter<boolean | undefined>
17
+ }
18
+
19
+ /**
20
+ * Renders the shared outer Positioner element used by popup components.
21
+ * Applies the common role, hidden state, transition styles, state attributes, and optional inert styling.
22
+ */
23
+ export function usePositioner<State extends Record<string, any>>(
24
+ componentProps: UseRenderElementComponentProps<State>,
25
+ state: ComputedRef<State> | Ref<State>,
26
+ {
27
+ styles,
28
+ transitionStatus,
29
+ props,
30
+ refs,
31
+ hidden,
32
+ inert = false,
33
+ }: UsePositionerOptions,
34
+ ) {
35
+ const positionerProps = computed(() => {
36
+ const isInert = toValue(inert) ?? false
37
+
38
+ return mergeProps(
39
+ {
40
+ role: 'presentation',
41
+ hidden: toValue(hidden) ? '' : undefined,
42
+ inert: isInert ? '' : undefined,
43
+ style: [
44
+ toValue(styles),
45
+ isInert ? { pointerEvents: 'none' } : undefined,
46
+ ],
47
+ },
48
+ getDisabledMountTransitionStyles(toValue(transitionStatus)),
49
+ toValue(props),
50
+ )
51
+ })
52
+
53
+ return useRenderElement({
54
+ componentProps,
55
+ state,
56
+ props: positionerProps,
57
+ stateAttributesMapping: popupStateMapping,
58
+ defaultTagName: 'div',
59
+ ref: refs,
60
+ })
61
+ }