base-ui-vue 0.2.0 → 0.4.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/button/ToolbarButton.cjs +6 -0
- package/dist/button/ToolbarButton.js +1 -1
- package/dist/content/ScrollAreaContent.cjs +168 -0
- package/dist/content/ScrollAreaContent.cjs.map +1 -0
- package/dist/content/ScrollAreaContent.js +133 -0
- package/dist/content/ScrollAreaContent.js.map +1 -0
- package/dist/control/SliderControl.js +2 -2
- package/dist/corner/ScrollAreaCorner.cjs +77 -0
- package/dist/corner/ScrollAreaCorner.cjs.map +1 -0
- package/dist/corner/ScrollAreaCorner.js +72 -0
- package/dist/corner/ScrollAreaCorner.js.map +1 -0
- package/dist/decrement/NumberFieldDecrement.cjs +861 -0
- package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
- package/dist/decrement/NumberFieldDecrement.js +700 -0
- package/dist/decrement/NumberFieldDecrement.js.map +1 -0
- package/dist/fallback/AvatarFallback.cjs +2 -46
- package/dist/fallback/AvatarFallback.cjs.map +1 -1
- package/dist/fallback/AvatarFallback.js +3 -41
- package/dist/fallback/AvatarFallback.js.map +1 -1
- package/dist/group/NumberFieldGroup.cjs +72 -0
- package/dist/group/NumberFieldGroup.cjs.map +1 -0
- package/dist/group/NumberFieldGroup.js +67 -0
- package/dist/group/NumberFieldGroup.js.map +1 -0
- package/dist/increment/NumberFieldIncrement.cjs +112 -0
- package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
- package/dist/increment/NumberFieldIncrement.js +107 -0
- package/dist/increment/NumberFieldIncrement.js.map +1 -0
- package/dist/index.cjs +52 -0
- package/dist/index.d.cts +1761 -430
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1761 -430
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index2.cjs +4065 -60
- package/dist/index2.cjs.map +1 -1
- package/dist/index2.js +3955 -184
- package/dist/index2.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -0
- package/src/input/Input.vue +37 -0
- package/src/input/InputDataAttributes.ts +30 -0
- package/src/input/index.ts +4 -0
- package/src/meter/index.ts +16 -0
- package/src/meter/indicator/MeterIndicator.vue +65 -0
- package/src/meter/label/MeterLabel.vue +63 -0
- package/src/meter/root/MeterRoot.vue +131 -0
- package/src/meter/root/MeterRootContext.ts +41 -0
- package/src/meter/track/MeterTrack.vue +46 -0
- package/src/meter/value/MeterValue.vue +85 -0
- package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
- package/src/number-field/group/NumberFieldGroup.vue +47 -0
- package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
- package/src/number-field/index.ts +42 -0
- package/src/number-field/input/NumberFieldInput.vue +455 -0
- package/src/number-field/root/NumberFieldRoot.vue +626 -0
- package/src/number-field/root/NumberFieldRootContext.ts +94 -0
- package/src/number-field/root/useNumberFieldButton.ts +171 -0
- package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
- package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
- package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
- package/src/number-field/utils/constants.ts +4 -0
- package/src/number-field/utils/getViewportRect.ts +34 -0
- package/src/number-field/utils/parse.ts +248 -0
- package/src/number-field/utils/stateAttributesMapping.ts +9 -0
- package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
- package/src/number-field/utils/types.ts +24 -0
- package/src/number-field/utils/validate.ts +120 -0
- package/src/otp-field/index.ts +22 -0
- package/src/otp-field/input/OtpFieldInput.vue +336 -0
- package/src/otp-field/root/OtpFieldRoot.vue +583 -0
- package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
- package/src/otp-field/utils/otp.ts +135 -0
- package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
- package/src/progress/index.ts +23 -0
- package/src/progress/indicator/ProgressIndicator.vue +74 -0
- package/src/progress/label/ProgressLabel.vue +63 -0
- package/src/progress/root/ProgressRoot.vue +160 -0
- package/src/progress/root/ProgressRootContext.ts +51 -0
- package/src/progress/root/ProgressRootDataAttributes.ts +14 -0
- package/src/progress/root/stateAttributesMapping.ts +18 -0
- package/src/progress/track/ProgressTrack.vue +48 -0
- package/src/progress/value/ProgressValue.vue +92 -0
- package/src/scroll-area/constants.ts +2 -0
- package/src/scroll-area/content/ScrollAreaContent.vue +87 -0
- package/src/scroll-area/corner/ScrollAreaCorner.vue +64 -0
- package/src/scroll-area/index.ts +25 -0
- package/src/scroll-area/root/ScrollAreaRoot.vue +297 -0
- package/src/scroll-area/root/ScrollAreaRootContext.ts +89 -0
- package/src/scroll-area/root/ScrollAreaRootCssVars.ts +4 -0
- package/src/scroll-area/root/ScrollAreaRootDataAttributes.ts +9 -0
- package/src/scroll-area/root/stateAttributes.ts +14 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbar.vue +263 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarContext.ts +20 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarCssVars.ts +4 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarDataAttributes.ts +11 -0
- package/src/scroll-area/thumb/ScrollAreaThumb.vue +120 -0
- package/src/scroll-area/thumb/ScrollAreaThumbDataAttributes.ts +3 -0
- package/src/scroll-area/utils/getOffset.ts +34 -0
- package/src/scroll-area/viewport/ScrollAreaViewport.vue +379 -0
- package/src/scroll-area/viewport/ScrollAreaViewportContext.ts +20 -0
- package/src/scroll-area/viewport/ScrollAreaViewportCssVars.ts +6 -0
- package/src/scroll-area/viewport/ScrollAreaViewportDataAttributes.ts +9 -0
- package/src/utils/detectBrowser.ts +15 -0
- package/src/utils/formatNumber.ts +60 -2
- package/src/utils/scrollEdges.ts +33 -0
- package/src/utils/styles.ts +28 -0
- package/src/utils/useInterval.ts +45 -0
- package/src/utils/usePressAndHold.ts +260 -0
- package/src/utils/useValueChanged.ts +21 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
3
|
+
import type { HiddenState, ScrollAreaRootState } from '../root/ScrollAreaRootContext'
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, provide, useAttrs, watch } from 'vue'
|
|
5
|
+
import { useDirection } from '../../direction-provider/DirectionContext'
|
|
6
|
+
import { mergeProps } from '../../merge-props/mergeProps'
|
|
7
|
+
import { clamp } from '../../utils/clamp'
|
|
8
|
+
import { normalizeScrollOffset } from '../../utils/scrollEdges'
|
|
9
|
+
import { styleDisableScrollbar } from '../../utils/styles'
|
|
10
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
11
|
+
import { MIN_THUMB_SIZE } from '../constants'
|
|
12
|
+
import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
|
|
13
|
+
import { scrollAreaStateAttributesMapping } from '../root/stateAttributes'
|
|
14
|
+
import { getOffset } from '../utils/getOffset'
|
|
15
|
+
import { scrollAreaViewportContextKey } from './ScrollAreaViewportContext'
|
|
16
|
+
import { ScrollAreaViewportCssVars } from './ScrollAreaViewportCssVars'
|
|
17
|
+
|
|
18
|
+
export type ScrollAreaViewportState = ScrollAreaRootState
|
|
19
|
+
|
|
20
|
+
export interface ScrollAreaViewportProps extends BaseUIComponentProps<ScrollAreaViewportState> {}
|
|
21
|
+
|
|
22
|
+
defineOptions({
|
|
23
|
+
name: 'ScrollAreaViewport',
|
|
24
|
+
inheritAttrs: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<ScrollAreaViewportProps>(), {
|
|
28
|
+
as: 'div',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const attrs = useAttrs()
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
viewportRef,
|
|
35
|
+
scrollbarYRef,
|
|
36
|
+
scrollbarXRef,
|
|
37
|
+
thumbYRef,
|
|
38
|
+
thumbXRef,
|
|
39
|
+
cornerRef,
|
|
40
|
+
cornerSize,
|
|
41
|
+
setCornerSize,
|
|
42
|
+
setThumbSize,
|
|
43
|
+
rootId,
|
|
44
|
+
setHiddenState,
|
|
45
|
+
hiddenState,
|
|
46
|
+
setHasMeasuredScrollbar,
|
|
47
|
+
handleScroll,
|
|
48
|
+
setHovering,
|
|
49
|
+
setOverflowEdges,
|
|
50
|
+
overflowEdges,
|
|
51
|
+
overflowEdgeThreshold,
|
|
52
|
+
scrollingX,
|
|
53
|
+
scrollingY,
|
|
54
|
+
} = useScrollAreaRootContext()
|
|
55
|
+
|
|
56
|
+
const direction = useDirection()
|
|
57
|
+
|
|
58
|
+
let programmaticScroll = true
|
|
59
|
+
const lastMeasuredViewportMetrics: [number, number, number, number] = [Number.NaN, Number.NaN, Number.NaN, Number.NaN]
|
|
60
|
+
|
|
61
|
+
let scrollEndTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
62
|
+
let waitForAnimationsTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
63
|
+
let resizeObserver: ResizeObserver | undefined
|
|
64
|
+
|
|
65
|
+
let scrollAreaOverflowVarsRegistered = false
|
|
66
|
+
|
|
67
|
+
function removeCSSVariableInheritance() {
|
|
68
|
+
if (scrollAreaOverflowVarsRegistered)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
const isWebKit = typeof navigator !== 'undefined' && /AppleWebKit/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
|
|
72
|
+
if (isWebKit)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
|
|
76
|
+
const vars = [
|
|
77
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowXStart,
|
|
78
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowXEnd,
|
|
79
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowYStart,
|
|
80
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowYEnd,
|
|
81
|
+
]
|
|
82
|
+
for (const name of vars) {
|
|
83
|
+
try {
|
|
84
|
+
CSS.registerProperty({ name, syntax: '<length>', inherits: false, initialValue: '0px' })
|
|
85
|
+
}
|
|
86
|
+
catch { /* already registered */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
scrollAreaOverflowVarsRegistered = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function computeThumbPosition() {
|
|
94
|
+
const viewportEl = viewportRef.value
|
|
95
|
+
const scrollbarYEl = scrollbarYRef.value
|
|
96
|
+
const scrollbarXEl = scrollbarXRef.value
|
|
97
|
+
const thumbYEl = thumbYRef.value
|
|
98
|
+
const thumbXEl = thumbXRef.value
|
|
99
|
+
const cornerEl = cornerRef.value
|
|
100
|
+
|
|
101
|
+
if (!viewportEl)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
const scrollableContentHeight = viewportEl.scrollHeight
|
|
105
|
+
const scrollableContentWidth = viewportEl.scrollWidth
|
|
106
|
+
const viewportHeight = viewportEl.clientHeight
|
|
107
|
+
const viewportWidth = viewportEl.clientWidth
|
|
108
|
+
const scrollTop = viewportEl.scrollTop
|
|
109
|
+
const scrollLeft = viewportEl.scrollLeft
|
|
110
|
+
const isFirstMeasurement = Number.isNaN(lastMeasuredViewportMetrics[0])
|
|
111
|
+
lastMeasuredViewportMetrics[0] = viewportHeight
|
|
112
|
+
lastMeasuredViewportMetrics[1] = scrollableContentHeight
|
|
113
|
+
lastMeasuredViewportMetrics[2] = viewportWidth
|
|
114
|
+
lastMeasuredViewportMetrics[3] = scrollableContentWidth
|
|
115
|
+
|
|
116
|
+
if (isFirstMeasurement) {
|
|
117
|
+
setHasMeasuredScrollbar(true)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (scrollableContentHeight === 0 || scrollableContentWidth === 0)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
const nextHiddenState = getHiddenState(viewportEl)
|
|
124
|
+
const scrollbarYHidden = nextHiddenState.y
|
|
125
|
+
const scrollbarXHidden = nextHiddenState.x
|
|
126
|
+
const ratioX = viewportWidth / scrollableContentWidth
|
|
127
|
+
const ratioY = viewportHeight / scrollableContentHeight
|
|
128
|
+
const maxScrollLeft = Math.max(0, scrollableContentWidth - viewportWidth)
|
|
129
|
+
const maxScrollTop = Math.max(0, scrollableContentHeight - viewportHeight)
|
|
130
|
+
|
|
131
|
+
let scrollLeftFromStart = 0
|
|
132
|
+
let scrollLeftFromEnd = 0
|
|
133
|
+
if (!scrollbarXHidden) {
|
|
134
|
+
let rawScrollLeftFromStart = 0
|
|
135
|
+
if (direction.value === 'rtl') {
|
|
136
|
+
rawScrollLeftFromStart = scrollLeft < 0 ? -scrollLeft : maxScrollLeft - scrollLeft
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
rawScrollLeftFromStart = scrollLeft
|
|
140
|
+
}
|
|
141
|
+
rawScrollLeftFromStart = clamp(rawScrollLeftFromStart, 0, maxScrollLeft)
|
|
142
|
+
scrollLeftFromStart = normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft)
|
|
143
|
+
scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rawScrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0
|
|
147
|
+
const scrollTopFromStart = !scrollbarYHidden ? normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0
|
|
148
|
+
const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0
|
|
149
|
+
const nextWidth = scrollbarXHidden ? 0 : viewportWidth
|
|
150
|
+
const nextHeight = scrollbarYHidden ? 0 : viewportHeight
|
|
151
|
+
|
|
152
|
+
let nextCornerWidth = 0
|
|
153
|
+
let nextCornerHeight = 0
|
|
154
|
+
if (!scrollbarXHidden && !scrollbarYHidden) {
|
|
155
|
+
nextCornerWidth = scrollbarYEl?.offsetWidth || 0
|
|
156
|
+
nextCornerHeight = scrollbarXEl?.offsetHeight || 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cornerNotYetSized = cornerSize.value.width === 0 && cornerSize.value.height === 0
|
|
160
|
+
const cornerWidthOffset = cornerNotYetSized ? nextCornerWidth : 0
|
|
161
|
+
const cornerHeightOffset = cornerNotYetSized ? nextCornerHeight : 0
|
|
162
|
+
|
|
163
|
+
const scrollbarXOffset = getOffset(scrollbarXEl, 'padding', 'x')
|
|
164
|
+
const scrollbarYOffset = getOffset(scrollbarYEl, 'padding', 'y')
|
|
165
|
+
const thumbXOffset = getOffset(thumbXEl, 'margin', 'x')
|
|
166
|
+
const thumbYOffset = getOffset(thumbYEl, 'margin', 'y')
|
|
167
|
+
|
|
168
|
+
const idealNextWidth = nextWidth - scrollbarXOffset - thumbXOffset
|
|
169
|
+
const idealNextHeight = nextHeight - scrollbarYOffset - thumbYOffset
|
|
170
|
+
|
|
171
|
+
const maxNextWidth = scrollbarXEl ? Math.min(scrollbarXEl.offsetWidth - cornerWidthOffset, idealNextWidth) : idealNextWidth
|
|
172
|
+
const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight
|
|
173
|
+
|
|
174
|
+
const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX)
|
|
175
|
+
const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY)
|
|
176
|
+
|
|
177
|
+
setThumbSize({ width: clampedNextWidth, height: clampedNextHeight })
|
|
178
|
+
|
|
179
|
+
if (scrollbarYEl && thumbYEl) {
|
|
180
|
+
const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset
|
|
181
|
+
const scrollRangeY = scrollableContentHeight - viewportHeight
|
|
182
|
+
const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY
|
|
183
|
+
const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY))
|
|
184
|
+
thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (scrollbarXEl && thumbXEl) {
|
|
188
|
+
const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset
|
|
189
|
+
const scrollRangeX = scrollableContentWidth - viewportWidth
|
|
190
|
+
const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeftFromStart / scrollRangeX
|
|
191
|
+
const thumbOffsetX = direction.value === 'rtl'
|
|
192
|
+
? -clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX)
|
|
193
|
+
: clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX)
|
|
194
|
+
thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const overflowMetricsPx: Array<[string, number]> = [
|
|
198
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart],
|
|
199
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd],
|
|
200
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart],
|
|
201
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd],
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for (const [cssVar, value] of overflowMetricsPx) {
|
|
205
|
+
viewportEl.style.setProperty(cssVar, `${value}px`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (cornerEl) {
|
|
209
|
+
if (scrollbarXHidden || scrollbarYHidden) {
|
|
210
|
+
setCornerSize({ width: 0, height: 0 })
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
setCornerSize({ width: nextCornerWidth, height: nextCornerHeight })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setHiddenState(mergeHiddenState(hiddenState.value, nextHiddenState))
|
|
218
|
+
|
|
219
|
+
const threshold = overflowEdgeThreshold.value
|
|
220
|
+
const nextOverflowEdges = {
|
|
221
|
+
xStart: !scrollbarXHidden && scrollLeftFromStart > threshold.xStart,
|
|
222
|
+
xEnd: !scrollbarXHidden && scrollLeftFromEnd > threshold.xEnd,
|
|
223
|
+
yStart: !scrollbarYHidden && scrollTopFromStart > threshold.yStart,
|
|
224
|
+
yEnd: !scrollbarYHidden && scrollTopFromEnd > threshold.yEnd,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const prev = overflowEdges.value
|
|
228
|
+
if (
|
|
229
|
+
prev.xStart !== nextOverflowEdges.xStart
|
|
230
|
+
|| prev.xEnd !== nextOverflowEdges.xEnd
|
|
231
|
+
|| prev.yStart !== nextOverflowEdges.yStart
|
|
232
|
+
|| prev.yEnd !== nextOverflowEdges.yEnd
|
|
233
|
+
) {
|
|
234
|
+
setOverflowEdges(nextOverflowEdges)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function handleUserInteraction() {
|
|
239
|
+
programmaticScroll = false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function onScroll() {
|
|
243
|
+
if (!viewportRef.value)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
computeThumbPosition()
|
|
247
|
+
|
|
248
|
+
if (!programmaticScroll) {
|
|
249
|
+
handleScroll({
|
|
250
|
+
x: viewportRef.value.scrollLeft,
|
|
251
|
+
y: viewportRef.value.scrollTop,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
clearTimeout(scrollEndTimeoutId)
|
|
256
|
+
scrollEndTimeoutId = setTimeout(() => {
|
|
257
|
+
programmaticScroll = true
|
|
258
|
+
}, 100)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const viewportState = computed<ScrollAreaViewportState>(() => ({
|
|
262
|
+
scrolling: scrollingX.value || scrollingY.value,
|
|
263
|
+
hasOverflowX: !hiddenState.value.x,
|
|
264
|
+
hasOverflowY: !hiddenState.value.y,
|
|
265
|
+
overflowXStart: overflowEdges.value.xStart,
|
|
266
|
+
overflowXEnd: overflowEdges.value.xEnd,
|
|
267
|
+
overflowYStart: overflowEdges.value.yStart,
|
|
268
|
+
overflowYEnd: overflowEdges.value.yEnd,
|
|
269
|
+
cornerHidden: hiddenState.value.corner,
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
const elementProps = computed(() => mergeProps(
|
|
273
|
+
attrs as Record<string, any>,
|
|
274
|
+
{
|
|
275
|
+
role: 'presentation',
|
|
276
|
+
...(rootId ? { 'data-id': `${rootId}-viewport` } : {}),
|
|
277
|
+
tabindex: hiddenState.value.x && hiddenState.value.y ? -1 : 0,
|
|
278
|
+
class: styleDisableScrollbar.className,
|
|
279
|
+
style: { overflow: 'scroll' },
|
|
280
|
+
onScroll,
|
|
281
|
+
onWheel: handleUserInteraction,
|
|
282
|
+
onTouchmove: handleUserInteraction,
|
|
283
|
+
onPointermove: handleUserInteraction,
|
|
284
|
+
onPointerenter: handleUserInteraction,
|
|
285
|
+
onKeydown: handleUserInteraction,
|
|
286
|
+
},
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
const {
|
|
290
|
+
tag,
|
|
291
|
+
mergedProps,
|
|
292
|
+
renderless,
|
|
293
|
+
ref: renderRef,
|
|
294
|
+
} = useRenderElement({
|
|
295
|
+
componentProps: props,
|
|
296
|
+
state: viewportState,
|
|
297
|
+
props: elementProps,
|
|
298
|
+
stateAttributesMapping: scrollAreaStateAttributesMapping,
|
|
299
|
+
defaultTagName: 'div',
|
|
300
|
+
ref: viewportRef,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
onMounted(() => {
|
|
304
|
+
removeCSSVariableInheritance()
|
|
305
|
+
|
|
306
|
+
if (viewportRef.value?.matches(':hover')) {
|
|
307
|
+
setHovering(true)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
queueMicrotask(computeThumbPosition)
|
|
311
|
+
|
|
312
|
+
const viewport = viewportRef.value
|
|
313
|
+
if (typeof ResizeObserver !== 'undefined' && viewport) {
|
|
314
|
+
let hasInitialized = false
|
|
315
|
+
resizeObserver = new ResizeObserver(() => {
|
|
316
|
+
if (!hasInitialized) {
|
|
317
|
+
hasInitialized = true
|
|
318
|
+
if (
|
|
319
|
+
lastMeasuredViewportMetrics[0] === viewport.clientHeight
|
|
320
|
+
&& lastMeasuredViewportMetrics[1] === viewport.scrollHeight
|
|
321
|
+
&& lastMeasuredViewportMetrics[2] === viewport.clientWidth
|
|
322
|
+
&& lastMeasuredViewportMetrics[3] === viewport.scrollWidth
|
|
323
|
+
) {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
computeThumbPosition()
|
|
328
|
+
})
|
|
329
|
+
resizeObserver.observe(viewport)
|
|
330
|
+
|
|
331
|
+
waitForAnimationsTimeoutId = setTimeout(() => {
|
|
332
|
+
const animations = viewport.getAnimations({ subtree: true })
|
|
333
|
+
if (animations.length === 0)
|
|
334
|
+
return
|
|
335
|
+
Promise.allSettled(animations.map(a => a.finished))
|
|
336
|
+
.then(computeThumbPosition)
|
|
337
|
+
}, 0)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
watch([hiddenState, direction], () => {
|
|
342
|
+
queueMicrotask(computeThumbPosition)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
watch(overflowEdgeThreshold, computeThumbPosition)
|
|
346
|
+
|
|
347
|
+
onBeforeUnmount(() => {
|
|
348
|
+
resizeObserver?.disconnect()
|
|
349
|
+
clearTimeout(scrollEndTimeoutId)
|
|
350
|
+
clearTimeout(waitForAnimationsTimeoutId)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
provide(scrollAreaViewportContextKey, { computeThumbPosition })
|
|
354
|
+
|
|
355
|
+
function getHiddenState(viewport: HTMLElement): HiddenState {
|
|
356
|
+
const y = viewport.clientHeight >= viewport.scrollHeight
|
|
357
|
+
const x = viewport.clientWidth >= viewport.scrollWidth
|
|
358
|
+
return { y, x, corner: y || x }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function mergeHiddenState(prevState: HiddenState, nextState: HiddenState): HiddenState {
|
|
362
|
+
if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
|
|
363
|
+
return prevState
|
|
364
|
+
}
|
|
365
|
+
return nextState
|
|
366
|
+
}
|
|
367
|
+
</script>
|
|
368
|
+
|
|
369
|
+
<template>
|
|
370
|
+
<slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="viewportState" />
|
|
371
|
+
<component
|
|
372
|
+
:is="tag"
|
|
373
|
+
v-else
|
|
374
|
+
:ref="renderRef"
|
|
375
|
+
v-bind="mergedProps"
|
|
376
|
+
>
|
|
377
|
+
<slot />
|
|
378
|
+
</component>
|
|
379
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ScrollAreaViewportContext {
|
|
5
|
+
computeThumbPosition: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const scrollAreaViewportContextKey = Symbol(
|
|
9
|
+
'ScrollAreaViewportContext',
|
|
10
|
+
) as InjectionKey<ScrollAreaViewportContext>
|
|
11
|
+
|
|
12
|
+
export function useScrollAreaViewportContext(): ScrollAreaViewportContext {
|
|
13
|
+
const context = inject(scrollAreaViewportContextKey)
|
|
14
|
+
if (context === undefined) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Base UI: ScrollAreaViewportContext is missing. ScrollArea.Content must be placed within <ScrollAreaViewport>.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export enum ScrollAreaViewportCssVars {
|
|
2
|
+
scrollAreaOverflowXStart = '--scroll-area-overflow-x-start',
|
|
3
|
+
scrollAreaOverflowXEnd = '--scroll-area-overflow-x-end',
|
|
4
|
+
scrollAreaOverflowYStart = '--scroll-area-overflow-y-start',
|
|
5
|
+
scrollAreaOverflowYEnd = '--scroll-area-overflow-y-end',
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export enum ScrollAreaViewportDataAttributes {
|
|
2
|
+
scrolling = 'data-scrolling',
|
|
3
|
+
hasOverflowX = 'data-has-overflow-x',
|
|
4
|
+
hasOverflowY = 'data-has-overflow-y',
|
|
5
|
+
overflowXStart = 'data-overflow-x-start',
|
|
6
|
+
overflowXEnd = 'data-overflow-x-end',
|
|
7
|
+
overflowYStart = 'data-overflow-y-start',
|
|
8
|
+
overflowYEnd = 'data-overflow-y-end',
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const ua = typeof navigator !== 'undefined' ? navigator.userAgent : ''
|
|
2
|
+
const platform = typeof navigator !== 'undefined' ? navigator.platform : ''
|
|
3
|
+
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints : 0
|
|
4
|
+
|
|
5
|
+
export const isWebKit
|
|
6
|
+
= typeof CSS !== 'undefined' && typeof CSS.supports === 'function'
|
|
7
|
+
? CSS.supports('-webkit-backdrop-filter:none') && !/chrome|android/i.test(ua)
|
|
8
|
+
: /\b(?:iphone|ipad|ipod)\b/i.test(ua)
|
|
9
|
+
|
|
10
|
+
export const isFirefox = /firefox/i.test(ua)
|
|
11
|
+
|
|
12
|
+
export const isIOS
|
|
13
|
+
= /\b(?:iphone|ipad|ipod)\b/i.test(ua)
|
|
14
|
+
// iPadOS reports as "MacIntel" but exposes touch points.
|
|
15
|
+
|| (platform === 'MacIntel' && maxTouchPoints > 1)
|
|
@@ -1,7 +1,65 @@
|
|
|
1
|
+
const formatterCache = new Map<string, Intl.NumberFormat>()
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a memoized `Intl.NumberFormat` for the given locale/options.
|
|
5
|
+
*/
|
|
6
|
+
export function getFormatter(locale?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) {
|
|
7
|
+
const optionsString = JSON.stringify({ locale, options })
|
|
8
|
+
const cachedFormatter = formatterCache.get(optionsString)
|
|
9
|
+
|
|
10
|
+
if (cachedFormatter) {
|
|
11
|
+
return cachedFormatter
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const formatter = new Intl.NumberFormat(locale, options)
|
|
15
|
+
formatterCache.set(optionsString, formatter)
|
|
16
|
+
|
|
17
|
+
return formatter
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
export function formatNumber(
|
|
2
|
-
value: number,
|
|
21
|
+
value: number | null,
|
|
3
22
|
locale?: Intl.LocalesArgument,
|
|
4
23
|
options?: Intl.NumberFormatOptions,
|
|
5
24
|
) {
|
|
6
|
-
|
|
25
|
+
if (value == null) {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
return getFormatter(locale, options).format(value)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function formatNumberMaxPrecision(
|
|
32
|
+
value: number | null,
|
|
33
|
+
locale?: Intl.LocalesArgument,
|
|
34
|
+
options?: Intl.NumberFormatOptions,
|
|
35
|
+
) {
|
|
36
|
+
return formatNumber(value, locale, {
|
|
37
|
+
...options,
|
|
38
|
+
maximumFractionDigits: 20,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats a numeric value for display inside Base UI Vue components.
|
|
44
|
+
*
|
|
45
|
+
* When no `format` is provided, the value is interpreted as a percentage
|
|
46
|
+
* in the 0-100 range (matching React's Base UI semantics for
|
|
47
|
+
* `<Meter.Root>` / `<Progress.Root>`).
|
|
48
|
+
*
|
|
49
|
+
* Returns an empty string when the value is `null`.
|
|
50
|
+
*/
|
|
51
|
+
export function formatNumberValue(
|
|
52
|
+
value: number | null,
|
|
53
|
+
locale?: Intl.LocalesArgument,
|
|
54
|
+
format?: Intl.NumberFormatOptions,
|
|
55
|
+
): string {
|
|
56
|
+
if (value == null) {
|
|
57
|
+
return ''
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!format) {
|
|
61
|
+
return formatNumber(value / 100, locale, { style: 'percent' })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return formatNumber(value, locale, format)
|
|
7
65
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { clamp } from './clamp'
|
|
2
|
+
|
|
3
|
+
export const SCROLL_EDGE_TOLERANCE_PX = 1
|
|
4
|
+
|
|
5
|
+
export function getMaxScrollOffset(scrollSize: number, clientSize: number) {
|
|
6
|
+
return Math.max(0, scrollSize - clientSize)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeScrollOffset(value: number, max: number) {
|
|
10
|
+
if (max <= 0) {
|
|
11
|
+
return 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const clamped = clamp(value, 0, max)
|
|
15
|
+
const startDistance = clamped
|
|
16
|
+
const endDistance = max - clamped
|
|
17
|
+
const withinStartTolerance = startDistance <= SCROLL_EDGE_TOLERANCE_PX
|
|
18
|
+
const withinEndTolerance = endDistance <= SCROLL_EDGE_TOLERANCE_PX
|
|
19
|
+
|
|
20
|
+
if (withinStartTolerance && withinEndTolerance) {
|
|
21
|
+
return startDistance <= endDistance ? 0 : max
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (withinStartTolerance) {
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (withinEndTolerance) {
|
|
29
|
+
return max
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return clamped
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const DISABLE_SCROLLBAR_CLASS_NAME = 'base-ui-disable-scrollbar'
|
|
2
|
+
const STYLE_ELEMENT_ID = `style-${DISABLE_SCROLLBAR_CLASS_NAME}`
|
|
3
|
+
|
|
4
|
+
export const styleDisableScrollbar = {
|
|
5
|
+
className: DISABLE_SCROLLBAR_CLASS_NAME,
|
|
6
|
+
|
|
7
|
+
inject(nonce?: string, disableStyleElements?: boolean) {
|
|
8
|
+
if (disableStyleElements) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof document === 'undefined') {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (document.getElementById(STYLE_ELEMENT_ID)) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const style = document.createElement('style')
|
|
21
|
+
style.id = STYLE_ELEMENT_ID
|
|
22
|
+
if (nonce) {
|
|
23
|
+
style.nonce = nonce
|
|
24
|
+
}
|
|
25
|
+
style.textContent = `.${DISABLE_SCROLLBAR_CLASS_NAME}{scrollbar-width:none}.${DISABLE_SCROLLBAR_CLASS_NAME}::-webkit-scrollbar{display:none}`
|
|
26
|
+
document.head.appendChild(style)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|