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,644 @@
1
+ import type { Rect } from '@floating-ui/utils'
2
+ import type { ComputedRef, CSSProperties, MaybeRefOrGetter, Ref, ShallowRef } from 'vue'
3
+ import type {
4
+ AutoUpdateOptions,
5
+ Boundary as FloatingBoundary,
6
+ Middleware,
7
+ MiddlewareState,
8
+ Padding,
9
+ Side as PhysicalSide,
10
+ Placement,
11
+ Strategy,
12
+ VirtualElement,
13
+ } from '../floating-ui-vue'
14
+ import {
15
+ getAlignment,
16
+ getSide,
17
+ getSideAxis,
18
+ } from '@floating-ui/utils'
19
+ import { computed, shallowRef, toValue, watchEffect } from 'vue'
20
+ import { useDirection } from '../direction-provider/DirectionContext'
21
+ import {
22
+ autoUpdate,
23
+ flip,
24
+ limitShift,
25
+ offset,
26
+ shift,
27
+ size,
28
+ useFloating,
29
+ } from '../floating-ui-vue'
30
+ import { arrow } from '../floating-ui-vue/middleware/arrow'
31
+ import { DEFAULT_SIDES } from './adaptiveOriginMiddleware'
32
+ import { hide } from './hideMiddleware'
33
+ import { ownerDocument, ownerWindow } from './owner'
34
+
35
+ export type Side = 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'
36
+ export type Align = 'start' | 'center' | 'end'
37
+ export type Boundary = 'clipping-ancestors' | Element | Element[] | Rect
38
+ export type OffsetFunction = (data: {
39
+ side: Side
40
+ align: Align
41
+ anchor: { width: number, height: number }
42
+ positioner: { width: number, height: number }
43
+ }) => number
44
+
45
+ interface SideFlipMode {
46
+ /**
47
+ * How to avoid collisions on the side axis.
48
+ * - `'flip'`: If there is not enough space, place the popup on the opposite side.
49
+ * - `'none'`: Keep the preferred side even if it overflows.
50
+ */
51
+ side?: 'flip' | 'none'
52
+ /**
53
+ * How to avoid collisions on the align axis.
54
+ * - `'flip'`: If there is not enough space, swap `'start'` and `'end'` alignment.
55
+ * - `'shift'`: Keep the alignment and shift the popup to fit within the boundary.
56
+ * - `'none'`: Keep the preferred alignment even if it overflows.
57
+ */
58
+ align?: 'flip' | 'shift' | 'none'
59
+ /**
60
+ * If both sides on the preferred axis do not fit, determines whether to fallback
61
+ * to a side on the perpendicular axis and which logical side to prefer.
62
+ * - `'start'`: Prefer the logical start side on the perpendicular axis.
63
+ * - `'end'`: Prefer the logical end side on the perpendicular axis.
64
+ * - `'none'`: Do not fallback to the perpendicular axis.
65
+ */
66
+ fallbackAxisSide?: 'start' | 'end' | 'none'
67
+ }
68
+
69
+ interface SideShiftMode {
70
+ /**
71
+ * How to avoid collisions on the side axis.
72
+ * - `'shift'`: Keep the preferred side and shift the popup to fit within the boundary.
73
+ * - `'none'`: Keep the preferred side even if it overflows.
74
+ */
75
+ side?: 'shift' | 'none'
76
+ /**
77
+ * How to avoid collisions on the align axis.
78
+ * - `'shift'`: Keep the alignment and shift the popup to fit within the boundary.
79
+ * - `'none'`: Keep the preferred alignment even if it overflows.
80
+ */
81
+ align?: 'shift' | 'none'
82
+ /**
83
+ * If both sides on the preferred axis do not fit, determines whether to fallback
84
+ * to a side on the perpendicular axis and which logical side to prefer.
85
+ * - `'start'`: Prefer the logical start side on the perpendicular axis.
86
+ * - `'end'`: Prefer the logical end side on the perpendicular axis.
87
+ * - `'none'`: Do not fallback to the perpendicular axis.
88
+ */
89
+ fallbackAxisSide?: 'start' | 'end' | 'none'
90
+ }
91
+
92
+ export type CollisionAvoidance = SideFlipMode | SideShiftMode
93
+
94
+ /**
95
+ * Used by regular popups that usually aren't scrollable and are allowed to
96
+ * freely flip to any axis of placement.
97
+ */
98
+ export const POPUP_COLLISION_AVOIDANCE: CollisionAvoidance = {
99
+ fallbackAxisSide: 'end',
100
+ }
101
+
102
+ /**
103
+ * Used for dropdowns that usually strictly prefer top/bottom placements and
104
+ * use `var(--available-height)` to limit their height.
105
+ */
106
+ export const DROPDOWN_COLLISION_AVOIDANCE: CollisionAvoidance = {
107
+ fallbackAxisSide: 'none',
108
+ }
109
+
110
+ export interface UseAnchorPositioningSharedParameters {
111
+ /**
112
+ * An element to position the popup against.
113
+ * By default, the popup will be positioned against the trigger.
114
+ */
115
+ anchor?: MaybeRefOrGetter<Element | VirtualElement | null | undefined>
116
+ /**
117
+ * Determines which CSS `position` property to use.
118
+ * @default 'absolute'
119
+ */
120
+ positionMethod?: MaybeRefOrGetter<Strategy | undefined>
121
+ /**
122
+ * Which side of the anchor element to align the popup against.
123
+ * May automatically change to avoid collisions.
124
+ * @default 'bottom'
125
+ */
126
+ side?: MaybeRefOrGetter<Side | undefined>
127
+ /**
128
+ * Distance between the anchor and the popup in pixels.
129
+ * Also accepts a function that returns the distance to read the dimensions of the anchor
130
+ * and positioner elements, along with its side and alignment.
131
+ *
132
+ * The function takes a `data` object parameter with the following properties:
133
+ * - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
134
+ * - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
135
+ * - `data.side`: which side of the anchor element the positioner is aligned against.
136
+ * - `data.align`: how the positioner is aligned relative to the specified side.
137
+ *
138
+ * @example
139
+ * ```vue
140
+ * <TooltipPositioner
141
+ * :side-offset="({ side, anchor }) => side === 'top' ? anchor.height : 0"
142
+ * />
143
+ * ```
144
+ *
145
+ * @default 0
146
+ */
147
+ sideOffset?: MaybeRefOrGetter<number | OffsetFunction | undefined>
148
+ /**
149
+ * How to align the popup relative to the specified side.
150
+ * @default 'center'
151
+ */
152
+ align?: MaybeRefOrGetter<Align | undefined>
153
+ /**
154
+ * Additional offset along the alignment axis in pixels.
155
+ * Also accepts a function that returns the offset to read the dimensions of the anchor
156
+ * and positioner elements, along with its side and alignment.
157
+ *
158
+ * The function takes a `data` object parameter with the following properties:
159
+ * - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`.
160
+ * - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`.
161
+ * - `data.side`: which side of the anchor element the positioner is aligned against.
162
+ * - `data.align`: how the positioner is aligned relative to the specified side.
163
+ *
164
+ * @example
165
+ * ```vue
166
+ * <TooltipPositioner
167
+ * :align-offset="({ align, positioner }) => align === 'start' ? positioner.width : 0"
168
+ * />
169
+ * ```
170
+ *
171
+ * @default 0
172
+ */
173
+ alignOffset?: MaybeRefOrGetter<number | OffsetFunction | undefined>
174
+ /**
175
+ * An element or a rectangle that delimits the area that the popup is confined to.
176
+ * @default 'clipping-ancestors'
177
+ */
178
+ collisionBoundary?: MaybeRefOrGetter<Boundary | undefined>
179
+ /**
180
+ * Additional space to maintain from the edge of the collision boundary.
181
+ * @default 5
182
+ */
183
+ collisionPadding?: MaybeRefOrGetter<Padding | undefined>
184
+ /**
185
+ * Whether to maintain the popup in the viewport after
186
+ * the anchor element was scrolled out of view.
187
+ * @default false
188
+ */
189
+ sticky?: MaybeRefOrGetter<boolean | undefined>
190
+ /**
191
+ * Minimum distance to maintain between the arrow and the edges of the popup.
192
+ *
193
+ * Use it to prevent the arrow element from hanging out of the rounded corners of a popup.
194
+ * @default 5
195
+ */
196
+ arrowPadding?: MaybeRefOrGetter<Padding | undefined>
197
+ /**
198
+ * Whether to disable the popup from tracking any layout shift of its positioning anchor.
199
+ * @default false
200
+ */
201
+ disableAnchorTracking?: MaybeRefOrGetter<boolean | undefined>
202
+ /**
203
+ * Determines how to handle collisions when positioning the popup.
204
+ */
205
+ collisionAvoidance?: MaybeRefOrGetter<CollisionAvoidance | undefined>
206
+ }
207
+
208
+ export interface UseAnchorPositioningParameters extends UseAnchorPositioningSharedParameters {
209
+ keepMounted?: MaybeRefOrGetter<boolean | undefined>
210
+ mounted: MaybeRefOrGetter<boolean>
211
+ shiftCrossAxis?: MaybeRefOrGetter<boolean | undefined>
212
+ lazyFlip?: MaybeRefOrGetter<boolean | undefined>
213
+ adaptiveOrigin?: MaybeRefOrGetter<Middleware | undefined>
214
+ autoUpdateOptions?: MaybeRefOrGetter<Partial<AutoUpdateOptions> | undefined>
215
+ positionerSizeVars?: MaybeRefOrGetter<boolean | undefined>
216
+ /**
217
+ * Optional middleware that can replace the measured reference rect before offsets and collision
218
+ * middleware run.
219
+ */
220
+ inline?: MaybeRefOrGetter<Middleware | undefined>
221
+ }
222
+
223
+ export interface UseAnchorPositioningReturnValue {
224
+ positionerRef: ShallowRef<HTMLElement | null>
225
+ positionerStyles: ComputedRef<CSSProperties>
226
+ arrowStyles: ComputedRef<CSSProperties>
227
+ arrowRef: Ref<HTMLElement | null>
228
+ arrowUncentered: ComputedRef<boolean>
229
+ side: ComputedRef<Side>
230
+ align: ComputedRef<Align>
231
+ physicalSide: ComputedRef<PhysicalSide>
232
+ anchorHidden: ComputedRef<boolean>
233
+ isPositioned: ComputedRef<boolean>
234
+ update: () => void
235
+ }
236
+
237
+ /**
238
+ * Provides standardized anchor positioning behavior for floating elements.
239
+ * Wraps Floating UI's Vue `useFloating` composable.
240
+ */
241
+ export function useAnchorPositioning(
242
+ params: UseAnchorPositioningParameters,
243
+ ): UseAnchorPositioningReturnValue {
244
+ const direction = useDirection()
245
+ const isRtl = computed(() => direction.value === 'rtl')
246
+ const mounted = computed(() => toValue(params.mounted))
247
+ const keepMounted = computed(() => toValue(params.keepMounted) ?? false)
248
+ const sideParam = computed(() => toValue(params.side) ?? 'bottom')
249
+ const alignParam = computed(() => toValue(params.align) ?? 'center')
250
+ const positionMethod = computed(() => toValue(params.positionMethod) ?? 'absolute')
251
+ const adaptiveOriginMiddleware = computed(() => toValue(params.adaptiveOrigin))
252
+ const positionerSizeVars = computed(() => toValue(params.positionerSizeVars) ?? true)
253
+ const collisionAvoidance = computed(() =>
254
+ toValue(params.collisionAvoidance) ?? POPUP_COLLISION_AVOIDANCE,
255
+ )
256
+
257
+ const mountSide = shallowRef<PhysicalSide | null>(null)
258
+ const positionerRef = shallowRef<HTMLElement | null>(null)
259
+ const arrowRef = shallowRef<HTMLElement | null>(null)
260
+ const transformOrigin = shallowRef<string | undefined>(undefined)
261
+ const sizeData = shallowRef<{
262
+ availableWidth?: number
263
+ availableHeight?: number
264
+ anchorWidth?: number
265
+ anchorHeight?: number
266
+ positionerWidth?: number
267
+ positionerHeight?: number
268
+ }>({})
269
+
270
+ watchEffect(() => {
271
+ if (!mounted.value && mountSide.value !== null) {
272
+ mountSide.value = null
273
+ }
274
+ })
275
+
276
+ const requestedPhysicalSide = computed<PhysicalSide>(() =>
277
+ mountSide.value ?? getPhysicalSide(sideParam.value, isRtl.value),
278
+ )
279
+
280
+ const placement = computed<Placement>(() => {
281
+ if (alignParam.value === 'center') {
282
+ return requestedPhysicalSide.value
283
+ }
284
+
285
+ return `${requestedPhysicalSide.value}-${alignParam.value}` as Placement
286
+ })
287
+
288
+ const reference = computed(() => toValue(params.anchor) ?? null)
289
+
290
+ const collisionPadding = computed(() =>
291
+ normalizeCollisionPadding(toValue(params.collisionPadding) ?? 5, sideParam.value),
292
+ )
293
+
294
+ const commonCollisionProps = computed(() => ({
295
+ boundary: getFloatingBoundary(toValue(params.collisionBoundary)),
296
+ padding: collisionPadding.value,
297
+ }))
298
+
299
+ const autoUpdateOptions = computed<AutoUpdateOptions>(() => ({
300
+ elementResize:
301
+ !toValue(params.disableAnchorTracking) && typeof ResizeObserver !== 'undefined',
302
+ layoutShift:
303
+ !toValue(params.disableAnchorTracking) && typeof IntersectionObserver !== 'undefined',
304
+ ...toValue(params.autoUpdateOptions),
305
+ }))
306
+
307
+ const middleware = computed<Middleware[]>(() => {
308
+ const sideOffset = toValue(params.sideOffset) ?? 0
309
+ const alignOffset = toValue(params.alignOffset) ?? 0
310
+ const arrowPadding = toValue(params.arrowPadding) ?? 5
311
+ const sticky = toValue(params.sticky) ?? false
312
+ const shiftCrossAxis = toValue(params.shiftCrossAxis) ?? false
313
+ const avoidance = collisionAvoidance.value
314
+ const collisionAvoidanceSide = avoidance.side ?? 'flip'
315
+ const collisionAvoidanceAlign = avoidance.align ?? 'flip'
316
+ const collisionAvoidanceFallbackAxisSide = avoidance.fallbackAxisSide ?? 'end'
317
+ const shiftDisabled
318
+ = collisionAvoidanceAlign === 'none' && collisionAvoidanceSide !== 'shift'
319
+ const crossAxisShiftEnabled
320
+ = !shiftDisabled && (sticky || shiftCrossAxis || collisionAvoidanceSide === 'shift')
321
+
322
+ const flipMiddleware = collisionAvoidanceSide === 'none'
323
+ ? null
324
+ : flip({
325
+ ...commonCollisionProps.value,
326
+ padding: expandCollisionPadding(collisionPadding.value, 1),
327
+ mainAxis: !shiftCrossAxis && collisionAvoidanceSide === 'flip',
328
+ crossAxis: collisionAvoidanceAlign === 'flip' ? 'alignment' : false,
329
+ fallbackAxisSideDirection: collisionAvoidanceFallbackAxisSide,
330
+ })
331
+
332
+ const shiftMiddleware = shiftDisabled
333
+ ? null
334
+ : shift((data) => {
335
+ const html = ownerDocument(data.elements.floating)?.documentElement
336
+
337
+ return {
338
+ ...commonCollisionProps.value,
339
+ rootBoundary: shiftCrossAxis && html
340
+ ? { x: 0, y: 0, width: html.clientWidth, height: html.clientHeight }
341
+ : undefined,
342
+ mainAxis: collisionAvoidanceAlign !== 'none',
343
+ crossAxis: crossAxisShiftEnabled,
344
+ limiter: sticky || shiftCrossAxis
345
+ ? undefined
346
+ : limitShift((limitData) => {
347
+ if (!arrowRef.value) {
348
+ return {}
349
+ }
350
+
351
+ const { width, height } = arrowRef.value.getBoundingClientRect()
352
+ const sideAxis = getSideAxis(getSide(limitData.placement))
353
+ const arrowSize = sideAxis === 'y' ? width : height
354
+ const offsetAmount = sideAxis === 'y'
355
+ ? collisionPadding.value.left + collisionPadding.value.right
356
+ : collisionPadding.value.top + collisionPadding.value.bottom
357
+
358
+ return {
359
+ offset: arrowSize / 2 + offsetAmount / 2,
360
+ }
361
+ }),
362
+ }
363
+ })
364
+
365
+ const middlewareItems: Array<Middleware | null | undefined> = [
366
+ toValue(params.inline),
367
+ offset((state) => {
368
+ const data = getOffsetData(state, sideParam.value, isRtl.value)
369
+ const sideAxis = typeof sideOffset === 'function' ? sideOffset(data) : sideOffset
370
+ const alignAxis = typeof alignOffset === 'function' ? alignOffset(data) : alignOffset
371
+
372
+ return {
373
+ mainAxis: sideAxis,
374
+ crossAxis: alignAxis,
375
+ alignmentAxis: alignAxis,
376
+ }
377
+ }),
378
+ ]
379
+
380
+ if (
381
+ collisionAvoidanceSide === 'shift'
382
+ || collisionAvoidanceAlign === 'shift'
383
+ || alignParam.value === 'center'
384
+ ) {
385
+ middlewareItems.push(shiftMiddleware, flipMiddleware)
386
+ }
387
+ else {
388
+ middlewareItems.push(flipMiddleware, shiftMiddleware)
389
+ }
390
+
391
+ middlewareItems.push(
392
+ size({
393
+ ...commonCollisionProps.value,
394
+ apply({ elements: { floating }, availableWidth, availableHeight, rects }) {
395
+ if (!mounted.value) {
396
+ return
397
+ }
398
+
399
+ const dpr = ownerWindow(floating).devicePixelRatio || 1
400
+ const { x, y, width, height } = rects.reference
401
+ const anchorWidth = (Math.round((x + width) * dpr) - Math.round(x * dpr)) / dpr
402
+ const anchorHeight = (Math.round((y + height) * dpr) - Math.round(y * dpr)) / dpr
403
+
404
+ sizeData.value = {
405
+ availableWidth,
406
+ availableHeight,
407
+ anchorWidth,
408
+ anchorHeight,
409
+ positionerWidth: rects.floating.width,
410
+ positionerHeight: rects.floating.height,
411
+ }
412
+ },
413
+ }),
414
+ arrow(() => ({
415
+ element:
416
+ arrowRef.value
417
+ || ownerDocument(positionerRef.value)?.createElement('div')
418
+ || undefined,
419
+ padding: arrowPadding,
420
+ offsetParent: 'floating',
421
+ })),
422
+ {
423
+ name: 'transformOrigin',
424
+ fn(state) {
425
+ const { elements, middlewareData, placement: renderedPlacement, rects, y } = state
426
+ const currentRenderedSide = getSide(renderedPlacement)
427
+ const currentRenderedAxis = getSideAxis(currentRenderedSide)
428
+ const arrowEl = arrowRef.value
429
+ const arrowX = middlewareData.arrow?.x || 0
430
+ const arrowY = middlewareData.arrow?.y || 0
431
+ const arrowWidth = arrowEl?.clientWidth || 0
432
+ const arrowHeight = arrowEl?.clientHeight || 0
433
+ const transformX = arrowX + arrowWidth / 2
434
+ const transformY = arrowY + arrowHeight / 2
435
+ const shiftY = Math.abs(middlewareData.shift?.y || 0)
436
+ const halfAnchorHeight = rects.reference.height / 2
437
+ const sideOffsetValue = typeof sideOffset === 'function'
438
+ ? sideOffset(getOffsetData(state, sideParam.value, isRtl.value))
439
+ : sideOffset
440
+ const isOverlappingAnchor = shiftY > sideOffsetValue
441
+
442
+ const adjacentTransformOrigin = {
443
+ top: `${transformX}px calc(100% + ${sideOffsetValue}px)`,
444
+ bottom: `${transformX}px ${-sideOffsetValue}px`,
445
+ left: `calc(100% + ${sideOffsetValue}px) ${transformY}px`,
446
+ right: `${-sideOffsetValue}px ${transformY}px`,
447
+ }[currentRenderedSide]
448
+ const overlapTransformOrigin
449
+ = `${transformX}px ${rects.reference.y + halfAnchorHeight - y}px`
450
+
451
+ transformOrigin.value
452
+ = crossAxisShiftEnabled && currentRenderedAxis === 'y' && isOverlappingAnchor
453
+ ? overlapTransformOrigin
454
+ : adjacentTransformOrigin
455
+
456
+ elements.floating.style.setProperty('--transform-origin', transformOrigin.value)
457
+
458
+ return {}
459
+ },
460
+ },
461
+ hide,
462
+ toValue(params.adaptiveOrigin),
463
+ )
464
+
465
+ return middlewareItems.filter((item): item is Middleware => item != null)
466
+ })
467
+
468
+ const floating = useFloating(reference, positionerRef, {
469
+ open: () => keepMounted.value ? mounted.value : undefined,
470
+ placement,
471
+ strategy: positionMethod,
472
+ middleware,
473
+ transform: () => adaptiveOriginMiddleware.value == null,
474
+ whileElementsMounted(referenceEl, floatingEl, update) {
475
+ return autoUpdate(referenceEl, floatingEl, update, autoUpdateOptions.value)
476
+ },
477
+ })
478
+
479
+ const renderedPhysicalSide = computed(() => getSide(floating.placement.value))
480
+ const side = computed<Side>(() =>
481
+ getLogicalSide(sideParam.value, renderedPhysicalSide.value, isRtl.value),
482
+ )
483
+ const align = computed<Align>(() =>
484
+ (getAlignment(floating.placement.value) as Align | undefined) ?? 'center',
485
+ )
486
+ const anchorHidden = computed(() =>
487
+ Boolean(floating.middlewareData.value.hide?.referenceHidden),
488
+ )
489
+ const arrowStyles = computed<CSSProperties>(() => ({
490
+ position: 'absolute',
491
+ left: toCssPixel(floating.middlewareData.value.arrow?.x),
492
+ top: toCssPixel(floating.middlewareData.value.arrow?.y),
493
+ }))
494
+ const arrowUncentered = computed(() => {
495
+ const centerOffset = floating.middlewareData.value.arrow?.centerOffset
496
+ return typeof centerOffset === 'number' && centerOffset !== 0
497
+ })
498
+ const resolvedPosition = computed<Strategy>(() =>
499
+ floating.isPositioned.value ? positionMethod.value : 'fixed',
500
+ )
501
+ const positionerStyles = computed<CSSProperties>(() => {
502
+ const adaptiveOriginData = floating.middlewareData.value.adaptiveOrigin as
503
+ | typeof DEFAULT_SIDES
504
+ | undefined
505
+ const { sideX, sideY } = adaptiveOriginData || DEFAULT_SIDES
506
+ const baseStyles: CSSProperties = adaptiveOriginMiddleware.value
507
+ ? {
508
+ position: resolvedPosition.value,
509
+ [sideX]: toCssPixel(floating.x.value),
510
+ [sideY]: toCssPixel(floating.y.value),
511
+ }
512
+ : {
513
+ ...floating.floatingStyles.value,
514
+ position: resolvedPosition.value,
515
+ }
516
+
517
+ const styles: CSSProperties = {
518
+ ...baseStyles,
519
+ '--available-width': toCssPixel(sizeData.value.availableWidth),
520
+ '--available-height': toCssPixel(sizeData.value.availableHeight),
521
+ '--anchor-width': toCssPixel(sizeData.value.anchorWidth),
522
+ '--anchor-height': toCssPixel(sizeData.value.anchorHeight),
523
+ '--transform-origin': transformOrigin.value,
524
+ }
525
+
526
+ if (positionerSizeVars.value) {
527
+ styles['--positioner-width'] = toCssPixel(sizeData.value.positionerWidth)
528
+ styles['--positioner-height'] = toCssPixel(sizeData.value.positionerHeight)
529
+ }
530
+
531
+ if (!floating.isPositioned.value) {
532
+ styles.opacity = 0
533
+ }
534
+
535
+ return styles
536
+ })
537
+
538
+ watchEffect(() => {
539
+ if (!toValue(params.lazyFlip) || !mounted.value || !floating.isPositioned.value) {
540
+ return
541
+ }
542
+
543
+ if (mountSide.value !== renderedPhysicalSide.value) {
544
+ mountSide.value = renderedPhysicalSide.value
545
+ }
546
+ })
547
+
548
+ return {
549
+ positionerRef,
550
+ positionerStyles,
551
+ arrowStyles,
552
+ arrowRef,
553
+ arrowUncentered,
554
+ side,
555
+ align,
556
+ physicalSide: renderedPhysicalSide,
557
+ anchorHidden,
558
+ isPositioned: computed(() => floating.isPositioned.value),
559
+ update: floating.update,
560
+ }
561
+ }
562
+
563
+ function getLogicalSide(sideParam: Side, renderedSide: PhysicalSide, isRtl: boolean): Side {
564
+ const isLogicalSideParam = sideParam === 'inline-start' || sideParam === 'inline-end'
565
+ const logicalRight = isRtl ? 'inline-start' : 'inline-end'
566
+ const logicalLeft = isRtl ? 'inline-end' : 'inline-start'
567
+
568
+ return {
569
+ top: 'top',
570
+ right: isLogicalSideParam ? logicalRight : 'right',
571
+ bottom: 'bottom',
572
+ left: isLogicalSideParam ? logicalLeft : 'left',
573
+ }[renderedSide] as Side
574
+ }
575
+
576
+ function getPhysicalSide(side: Side, isRtl: boolean): PhysicalSide {
577
+ return {
578
+ 'top': 'top',
579
+ 'right': 'right',
580
+ 'bottom': 'bottom',
581
+ 'left': 'left',
582
+ 'inline-end': isRtl ? 'left' : 'right',
583
+ 'inline-start': isRtl ? 'right' : 'left',
584
+ }[side] as PhysicalSide
585
+ }
586
+
587
+ function getOffsetData(state: MiddlewareState, sideParam: Side, isRtl: boolean) {
588
+ const { rects, placement } = state
589
+
590
+ return {
591
+ side: getLogicalSide(sideParam, getSide(placement), isRtl),
592
+ align: (getAlignment(placement) as Align | undefined) ?? 'center',
593
+ anchor: { width: rects.reference.width, height: rects.reference.height },
594
+ positioner: { width: rects.floating.width, height: rects.floating.height },
595
+ } as const
596
+ }
597
+
598
+ function normalizeCollisionPadding(padding: Padding, sideParam: Side) {
599
+ const bias = 1
600
+ const biasTop = sideParam === 'bottom' ? bias : 0
601
+ const biasBottom = sideParam === 'top' ? bias : 0
602
+ const biasLeft = sideParam === 'right' ? bias : 0
603
+ const biasRight = sideParam === 'left' ? bias : 0
604
+
605
+ if (typeof padding === 'number') {
606
+ return {
607
+ top: padding + biasTop,
608
+ right: padding + biasRight,
609
+ bottom: padding + biasBottom,
610
+ left: padding + biasLeft,
611
+ }
612
+ }
613
+
614
+ return {
615
+ top: (padding.top || 0) + biasTop,
616
+ right: (padding.right || 0) + biasRight,
617
+ bottom: (padding.bottom || 0) + biasBottom,
618
+ left: (padding.left || 0) + biasLeft,
619
+ }
620
+ }
621
+
622
+ function expandCollisionPadding(
623
+ padding: ReturnType<typeof normalizeCollisionPadding>,
624
+ amount: number,
625
+ ) {
626
+ return {
627
+ top: padding.top + amount,
628
+ right: padding.right + amount,
629
+ bottom: padding.bottom + amount,
630
+ left: padding.left + amount,
631
+ }
632
+ }
633
+
634
+ function getFloatingBoundary(boundary: Boundary | undefined): FloatingBoundary | undefined {
635
+ if (boundary === 'clipping-ancestors') {
636
+ return 'clippingAncestors'
637
+ }
638
+
639
+ return boundary
640
+ }
641
+
642
+ function toCssPixel(value: number | undefined) {
643
+ return value === undefined ? undefined : `${value}px`
644
+ }
@@ -0,0 +1,45 @@
1
+ import { onUnmounted } from 'vue'
2
+
3
+ type IntervalId = number
4
+
5
+ const EMPTY = 0 as IntervalId
6
+
7
+ export class Interval {
8
+ currentId: IntervalId = EMPTY
9
+
10
+ static create() {
11
+ return new Interval()
12
+ }
13
+
14
+ /**
15
+ * Repeatedly executes `fn` every `delay` ms, clearing any previously scheduled interval.
16
+ */
17
+ start(delay: number, fn: () => void) {
18
+ this.clear()
19
+ this.currentId = setInterval(fn, delay) as unknown as IntervalId
20
+ }
21
+
22
+ isStarted() {
23
+ return this.currentId !== EMPTY
24
+ }
25
+
26
+ clear = () => {
27
+ if (this.currentId !== EMPTY) {
28
+ clearInterval(this.currentId)
29
+ this.currentId = EMPTY
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * A `setInterval` with automatic cleanup on unmount.
36
+ */
37
+ export function useInterval() {
38
+ const interval = Interval.create()
39
+
40
+ onUnmounted(() => {
41
+ interval.clear()
42
+ })
43
+
44
+ return interval
45
+ }