base-ui-vue 0.4.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.
- package/dist/arrow/TooltipArrow.cjs +609 -0
- package/dist/arrow/TooltipArrow.cjs.map +1 -0
- package/dist/arrow/TooltipArrow.js +478 -0
- package/dist/arrow/TooltipArrow.js.map +1 -0
- package/dist/button/Button.cjs +5 -317
- package/dist/button/Button.cjs.map +1 -1
- package/dist/button/Button.js +3 -249
- package/dist/button/Button.js.map +1 -1
- package/dist/button/ToolbarButton.cjs +13 -6
- package/dist/button/ToolbarButton.cjs.map +1 -1
- package/dist/button/ToolbarButton.js +3 -2
- package/dist/button/ToolbarButton.js.map +1 -1
- package/dist/checkbox/index.cjs +20 -9
- package/dist/checkbox/index.cjs.map +1 -1
- package/dist/checkbox/index.js +7 -2
- package/dist/checkbox/index.js.map +1 -1
- package/dist/checkbox-group/CheckboxGroup.cjs +14 -13
- package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -1
- package/dist/checkbox-group/CheckboxGroup.js +2 -1
- package/dist/checkbox-group/CheckboxGroup.js.map +1 -1
- package/dist/components/FloatingPortal.cjs +66 -0
- package/dist/components/FloatingPortal.cjs.map +1 -0
- package/dist/components/FloatingPortal.js +55 -0
- package/dist/components/FloatingPortal.js.map +1 -0
- package/dist/components/FloatingRootStore.cjs +101 -0
- package/dist/components/FloatingRootStore.cjs.map +1 -0
- package/dist/components/FloatingRootStore.js +84 -0
- package/dist/components/FloatingRootStore.js.map +1 -0
- package/dist/components/FloatingTree.cjs +33 -0
- package/dist/components/FloatingTree.cjs.map +1 -0
- package/dist/components/FloatingTree.js +22 -0
- package/dist/components/FloatingTree.js.map +1 -0
- package/dist/composite/composite.cjs +6 -0
- package/dist/composite/composite.js +1 -1
- package/dist/content/ScrollAreaContent.cjs +3 -3
- package/dist/content/ScrollAreaContent.cjs.map +1 -1
- package/dist/content/ScrollAreaContent.js +1 -1
- package/dist/control/FieldControl.cjs +3 -3
- package/dist/control/FieldControl.cjs.map +1 -1
- package/dist/control/FieldControl.js +1 -1
- package/dist/control/SliderControl.cjs +3 -3
- package/dist/control/SliderControl.cjs.map +1 -1
- package/dist/control/SliderControl.js +1 -1
- package/dist/corner/ScrollAreaCorner.cjs +3 -3
- package/dist/corner/ScrollAreaCorner.cjs.map +1 -1
- package/dist/corner/ScrollAreaCorner.js +1 -1
- package/dist/decrement/NumberFieldDecrement.cjs +10 -3
- package/dist/decrement/NumberFieldDecrement.cjs.map +1 -1
- package/dist/decrement/NumberFieldDecrement.js +3 -2
- package/dist/decrement/NumberFieldDecrement.js.map +1 -1
- package/dist/description/FieldDescription.cjs +2 -2
- package/dist/description/FieldDescription.cjs.map +1 -1
- package/dist/description/FieldDescription.js +1 -1
- package/dist/error/FieldError.cjs +2 -2
- package/dist/error/FieldError.cjs.map +1 -1
- package/dist/error/FieldError.js +1 -1
- package/dist/fallback/AvatarFallback.cjs +2 -2
- package/dist/fallback/AvatarFallback.cjs.map +1 -1
- package/dist/fallback/AvatarFallback.js +1 -1
- package/dist/floating-ui-vue/index.js +638 -0
- package/dist/floating-ui-vue/index.js.map +1 -0
- package/dist/form/Form.cjs +3 -3
- package/dist/form/Form.cjs.map +1 -1
- package/dist/form/Form.js +1 -1
- package/dist/group/NumberFieldGroup.cjs +3 -3
- package/dist/group/NumberFieldGroup.cjs.map +1 -1
- package/dist/group/NumberFieldGroup.js +1 -1
- package/dist/group/ToolbarGroup.cjs +2 -2
- package/dist/group/ToolbarGroup.cjs.map +1 -1
- package/dist/group/ToolbarGroup.js +1 -1
- package/dist/header/AccordionHeader.cjs +2 -2
- package/dist/header/AccordionHeader.cjs.map +1 -1
- package/dist/header/AccordionHeader.js +1 -1
- package/dist/hooks/useDismiss.cjs +502 -0
- package/dist/hooks/useDismiss.cjs.map +1 -0
- package/dist/hooks/useHoverFloatingInteraction.cjs +233 -0
- package/dist/hooks/useHoverFloatingInteraction.cjs.map +1 -0
- package/dist/image/AvatarImage.cjs +4 -4
- package/dist/image/AvatarImage.cjs.map +1 -1
- package/dist/image/AvatarImage.js +1 -1
- package/dist/increment/NumberFieldIncrement.cjs +3 -2
- package/dist/increment/NumberFieldIncrement.cjs.map +1 -1
- package/dist/increment/NumberFieldIncrement.js +2 -1
- package/dist/increment/NumberFieldIncrement.js.map +1 -1
- package/dist/index.cjs +60 -7
- package/dist/index.d.cts +3838 -1726
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3838 -1726
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index2.cjs +7475 -3089
- package/dist/index2.cjs.map +1 -1
- package/dist/index2.js +7215 -3106
- package/dist/index2.js.map +1 -1
- package/dist/rolldown-runtime.js +36 -0
- package/package.json +1 -1
- package/src/floating-ui-vue/components/FloatingPortal.ts +171 -0
- package/src/floating-ui-vue/components/FloatingPortal.vue +259 -0
- package/src/floating-ui-vue/components/FloatingRootStore.ts +83 -0
- package/src/floating-ui-vue/components/FloatingTree.ts +64 -0
- package/src/floating-ui-vue/components/FloatingTreeStore.ts +21 -0
- package/src/floating-ui-vue/hooks/useDismiss.ts +689 -0
- package/src/floating-ui-vue/hooks/useHoverFloatingInteraction.ts +244 -0
- package/src/floating-ui-vue/hooks/useHoverInteractionSharedState.ts +125 -0
- package/src/floating-ui-vue/hooks/useHoverShared.ts +53 -0
- package/src/floating-ui-vue/index.ts +48 -0
- package/src/floating-ui-vue/middleware/arrow.ts +114 -0
- package/src/floating-ui-vue/types.ts +117 -0
- package/src/floating-ui-vue/utils/composite.ts +20 -0
- package/src/floating-ui-vue/utils/constants.ts +2 -0
- package/src/floating-ui-vue/utils/createAttribute.ts +3 -0
- package/src/floating-ui-vue/utils/createEventEmitter.ts +20 -0
- package/src/floating-ui-vue/utils/element.ts +59 -0
- package/src/floating-ui-vue/utils/event.ts +15 -0
- package/src/floating-ui-vue/utils/nodes.ts +14 -0
- package/src/floating-ui-vue/utils/tabbable.ts +309 -0
- package/src/index.ts +4 -0
- package/src/radio/index.ts +28 -0
- package/src/radio/indicator/RadioIndicator.vue +90 -0
- package/src/radio/indicator/RadioIndicatorDataAttributes.ts +54 -0
- package/src/radio/root/RadioRoot.vue +544 -0
- package/src/radio/root/RadioRootContext.ts +18 -0
- package/src/radio/root/RadioRootDataAttributes.ts +46 -0
- package/src/radio/utils/stateAttributesMapping.ts +29 -0
- package/src/radio-group/RadioGroup.vue +381 -0
- package/src/radio-group/RadioGroupContext.ts +42 -0
- package/src/radio-group/RadioGroupDataAttributes.ts +6 -0
- package/src/radio-group/index.ts +10 -0
- package/src/tabs/index.ts +38 -0
- package/src/tabs/indicator/TabsIndicator.vue +201 -0
- package/src/tabs/indicator/TabsIndicatorCssVars.ts +26 -0
- package/src/tabs/indicator/TabsIndicatorDataAttributes.ts +12 -0
- package/src/tabs/indicator/prehydrationScript.min.ts +5 -0
- package/src/tabs/indicator/prehydrationScript.template.js +87 -0
- package/src/tabs/list/TabsList.vue +201 -0
- package/src/tabs/list/TabsListContext.ts +29 -0
- package/src/tabs/list/TabsListDataAttributes.ts +12 -0
- package/src/tabs/panel/TabsPanel.vue +167 -0
- package/src/tabs/panel/TabsPanelDataAttributes.ts +28 -0
- package/src/tabs/root/TabsRoot.vue +468 -0
- package/src/tabs/root/TabsRootContext.ts +56 -0
- package/src/tabs/root/TabsRootDataAttributes.ts +12 -0
- package/src/tabs/root/stateAttributesMapping.ts +9 -0
- package/src/tabs/tab/TabsTab.vue +274 -0
- package/src/tabs/tab/TabsTabDataAttributes.ts +20 -0
- package/src/tabs/utils/areTabValuesEqual.ts +6 -0
- package/src/tooltip/arrow/TooltipArrow.vue +116 -0
- package/src/tooltip/arrow/TooltipArrowDataAttributes.ts +31 -0
- package/src/tooltip/index.ts +54 -0
- package/src/tooltip/popup/TooltipPopup.vue +142 -0
- package/src/tooltip/popup/TooltipPopupDataAttributes.ts +35 -0
- package/src/tooltip/portal/TooltipPortal.vue +83 -0
- package/src/tooltip/portal/TooltipPortalContext.ts +19 -0
- package/src/tooltip/positioner/TooltipPositioner.vue +221 -0
- package/src/tooltip/positioner/TooltipPositionerContext.ts +23 -0
- package/src/tooltip/positioner/TooltipPositionerCssVars.ts +27 -0
- package/src/tooltip/positioner/TooltipPositionerDataAttributes.ts +26 -0
- package/src/tooltip/provider/TooltipProvider.vue +78 -0
- package/src/tooltip/provider/TooltipProviderContext.ts +19 -0
- package/src/tooltip/root/TooltipRoot.vue +401 -0
- package/src/tooltip/root/TooltipRootContext.ts +71 -0
- package/src/tooltip/store/TooltipHandle.ts +215 -0
- package/src/tooltip/trigger/TooltipTrigger.vue +343 -0
- package/src/tooltip/trigger/TooltipTriggerDataAttributes.ts +12 -0
- package/src/tooltip/utils/constants.ts +2 -0
- package/src/tooltip/viewport/TooltipViewport.vue +149 -0
- package/src/tooltip/viewport/TooltipViewportCssVars.ts +14 -0
- package/src/tooltip/viewport/TooltipViewportDataAttributes.ts +26 -0
- package/src/utils/FloatingPortalLite.vue +66 -0
- package/src/utils/FocusGuard.vue +36 -0
- package/src/utils/adaptiveOriginMiddleware.ts +79 -0
- package/src/utils/addEventListener.ts +16 -0
- package/src/utils/constants.ts +11 -0
- package/src/utils/createBaseUIEventDetails.ts +2 -0
- package/src/utils/getCssDimensions.ts +19 -0
- package/src/utils/getDisabledMountTransitionStyles.ts +9 -0
- package/src/utils/hideMiddleware.ts +19 -0
- package/src/utils/mergeCleanups.ts +7 -0
- package/src/utils/platform.ts +18 -0
- package/src/utils/popupStateMapping.ts +103 -0
- package/src/utils/reasons.ts +4 -0
- package/src/utils/serializeValue.ts +17 -0
- package/src/utils/useAnchorPositioning.ts +644 -0
- package/src/utils/usePopupAutoResize.ts +254 -0
- package/src/utils/usePopupViewport.ts +398 -0
- package/src/utils/usePositioner.ts +61 -0
- package/src/utils/useRenderElement.ts +3 -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
|
+
}
|