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.
Files changed (109) hide show
  1. package/dist/button/ToolbarButton.cjs +6 -0
  2. package/dist/button/ToolbarButton.js +1 -1
  3. package/dist/content/ScrollAreaContent.cjs +168 -0
  4. package/dist/content/ScrollAreaContent.cjs.map +1 -0
  5. package/dist/content/ScrollAreaContent.js +133 -0
  6. package/dist/content/ScrollAreaContent.js.map +1 -0
  7. package/dist/control/SliderControl.js +2 -2
  8. package/dist/corner/ScrollAreaCorner.cjs +77 -0
  9. package/dist/corner/ScrollAreaCorner.cjs.map +1 -0
  10. package/dist/corner/ScrollAreaCorner.js +72 -0
  11. package/dist/corner/ScrollAreaCorner.js.map +1 -0
  12. package/dist/decrement/NumberFieldDecrement.cjs +861 -0
  13. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  14. package/dist/decrement/NumberFieldDecrement.js +700 -0
  15. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  16. package/dist/fallback/AvatarFallback.cjs +2 -46
  17. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  18. package/dist/fallback/AvatarFallback.js +3 -41
  19. package/dist/fallback/AvatarFallback.js.map +1 -1
  20. package/dist/group/NumberFieldGroup.cjs +72 -0
  21. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  22. package/dist/group/NumberFieldGroup.js +67 -0
  23. package/dist/group/NumberFieldGroup.js.map +1 -0
  24. package/dist/increment/NumberFieldIncrement.cjs +112 -0
  25. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  26. package/dist/increment/NumberFieldIncrement.js +107 -0
  27. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  28. package/dist/index.cjs +52 -0
  29. package/dist/index.d.cts +1761 -430
  30. package/dist/index.d.cts.map +1 -1
  31. package/dist/index.d.ts +1761 -430
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -2
  34. package/dist/index2.cjs +4065 -60
  35. package/dist/index2.cjs.map +1 -1
  36. package/dist/index2.js +3955 -184
  37. package/dist/index2.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/index.ts +6 -0
  40. package/src/input/Input.vue +37 -0
  41. package/src/input/InputDataAttributes.ts +30 -0
  42. package/src/input/index.ts +4 -0
  43. package/src/meter/index.ts +16 -0
  44. package/src/meter/indicator/MeterIndicator.vue +65 -0
  45. package/src/meter/label/MeterLabel.vue +63 -0
  46. package/src/meter/root/MeterRoot.vue +131 -0
  47. package/src/meter/root/MeterRootContext.ts +41 -0
  48. package/src/meter/track/MeterTrack.vue +46 -0
  49. package/src/meter/value/MeterValue.vue +85 -0
  50. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  51. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  52. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  53. package/src/number-field/index.ts +42 -0
  54. package/src/number-field/input/NumberFieldInput.vue +455 -0
  55. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  56. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  57. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  58. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  59. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  60. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  61. package/src/number-field/utils/constants.ts +4 -0
  62. package/src/number-field/utils/getViewportRect.ts +34 -0
  63. package/src/number-field/utils/parse.ts +248 -0
  64. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  65. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  66. package/src/number-field/utils/types.ts +24 -0
  67. package/src/number-field/utils/validate.ts +120 -0
  68. package/src/otp-field/index.ts +22 -0
  69. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  70. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  71. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  72. package/src/otp-field/utils/otp.ts +135 -0
  73. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  74. package/src/progress/index.ts +23 -0
  75. package/src/progress/indicator/ProgressIndicator.vue +74 -0
  76. package/src/progress/label/ProgressLabel.vue +63 -0
  77. package/src/progress/root/ProgressRoot.vue +160 -0
  78. package/src/progress/root/ProgressRootContext.ts +51 -0
  79. package/src/progress/root/ProgressRootDataAttributes.ts +14 -0
  80. package/src/progress/root/stateAttributesMapping.ts +18 -0
  81. package/src/progress/track/ProgressTrack.vue +48 -0
  82. package/src/progress/value/ProgressValue.vue +92 -0
  83. package/src/scroll-area/constants.ts +2 -0
  84. package/src/scroll-area/content/ScrollAreaContent.vue +87 -0
  85. package/src/scroll-area/corner/ScrollAreaCorner.vue +64 -0
  86. package/src/scroll-area/index.ts +25 -0
  87. package/src/scroll-area/root/ScrollAreaRoot.vue +297 -0
  88. package/src/scroll-area/root/ScrollAreaRootContext.ts +89 -0
  89. package/src/scroll-area/root/ScrollAreaRootCssVars.ts +4 -0
  90. package/src/scroll-area/root/ScrollAreaRootDataAttributes.ts +9 -0
  91. package/src/scroll-area/root/stateAttributes.ts +14 -0
  92. package/src/scroll-area/scrollbar/ScrollAreaScrollbar.vue +263 -0
  93. package/src/scroll-area/scrollbar/ScrollAreaScrollbarContext.ts +20 -0
  94. package/src/scroll-area/scrollbar/ScrollAreaScrollbarCssVars.ts +4 -0
  95. package/src/scroll-area/scrollbar/ScrollAreaScrollbarDataAttributes.ts +11 -0
  96. package/src/scroll-area/thumb/ScrollAreaThumb.vue +120 -0
  97. package/src/scroll-area/thumb/ScrollAreaThumbDataAttributes.ts +3 -0
  98. package/src/scroll-area/utils/getOffset.ts +34 -0
  99. package/src/scroll-area/viewport/ScrollAreaViewport.vue +379 -0
  100. package/src/scroll-area/viewport/ScrollAreaViewportContext.ts +20 -0
  101. package/src/scroll-area/viewport/ScrollAreaViewportCssVars.ts +6 -0
  102. package/src/scroll-area/viewport/ScrollAreaViewportDataAttributes.ts +9 -0
  103. package/src/utils/detectBrowser.ts +15 -0
  104. package/src/utils/formatNumber.ts +60 -2
  105. package/src/utils/scrollEdges.ts +33 -0
  106. package/src/utils/styles.ts +28 -0
  107. package/src/utils/useInterval.ts +45 -0
  108. package/src/utils/usePressAndHold.ts +260 -0
  109. package/src/utils/useValueChanged.ts +21 -0
@@ -0,0 +1,171 @@
1
+ import type { ComputedRef, Ref } from 'vue'
2
+ import type {
3
+ Direction,
4
+ EventWithOptionalKeyState,
5
+ IncrementValueParameters,
6
+ } from '../utils/types'
7
+ import type {
8
+ NumberFieldRootChangeEventDetails,
9
+ NumberFieldRootChangeEventReason,
10
+ NumberFieldRootCommitEventDetails,
11
+ } from './NumberFieldRootContext'
12
+ import { computed } from 'vue'
13
+ import {
14
+ createChangeEventDetails,
15
+ createGenericEventDetails,
16
+ } from '../../utils/createBaseUIEventDetails'
17
+ import { REASONS } from '../../utils/reasons'
18
+ import { usePressAndHold } from '../../utils/usePressAndHold'
19
+ import {
20
+ CHANGE_VALUE_TICK_DELAY,
21
+ DEFAULT_STEP,
22
+ SCROLLING_POINTER_MOVE_DISTANCE,
23
+ START_AUTO_CHANGE_DELAY,
24
+ } from '../utils/constants'
25
+ import { parseNumber } from '../utils/parse'
26
+
27
+ // Treat pen as touch-like to avoid forcing the software keyboard on stylus taps.
28
+ function isTouchLikePointerType(pointerType: string) {
29
+ return pointerType === 'touch' || pointerType === 'pen'
30
+ }
31
+
32
+ export interface UseNumberFieldButtonParameters {
33
+ isIncrement: boolean
34
+ inputRef: Ref<HTMLInputElement | null>
35
+ inputValue: () => string
36
+ disabled: () => boolean
37
+ readOnly: () => boolean
38
+ id: () => string | undefined
39
+ setValue: (value: number | null, details: NumberFieldRootChangeEventDetails) => boolean
40
+ getStepAmount: (event?: EventWithOptionalKeyState) => number | undefined
41
+ incrementValue: (amount: number, params: IncrementValueParameters) => boolean
42
+ allowInputSyncRef: Ref<boolean>
43
+ formatOptionsRef: ComputedRef<Intl.NumberFormatOptions | undefined>
44
+ valueRef: Ref<number | null>
45
+ locale: () => Intl.LocalesArgument
46
+ lastChangedValueRef: Ref<number | null>
47
+ onValueCommitted: (value: number | null, eventDetails: NumberFieldRootCommitEventDetails) => void
48
+ }
49
+
50
+ export function useNumberFieldButton(
51
+ params: UseNumberFieldButtonParameters,
52
+ ): ComputedRef<Record<string, any>> {
53
+ const {
54
+ allowInputSyncRef,
55
+ formatOptionsRef,
56
+ getStepAmount,
57
+ incrementValue,
58
+ inputRef,
59
+ inputValue,
60
+ isIncrement,
61
+ locale,
62
+ setValue,
63
+ valueRef,
64
+ lastChangedValueRef,
65
+ onValueCommitted,
66
+ } = params
67
+
68
+ const disabled = () => params.disabled()
69
+ const readOnly = () => params.readOnly()
70
+
71
+ const pressReason: NumberFieldRootChangeEventReason = isIncrement
72
+ ? REASONS.incrementPress
73
+ : REASONS.decrementPress
74
+
75
+ function commitValue(nativeEvent: MouseEvent) {
76
+ allowInputSyncRef.value = true
77
+
78
+ // The input may be dirty but not yet blurred, so the value won't have been committed.
79
+ const parsedValue = parseNumber(inputValue(), locale(), formatOptionsRef.value)
80
+
81
+ if (parsedValue !== null) {
82
+ // The increment value function needs to know the current input value to increment it
83
+ // correctly.
84
+ valueRef.value = parsedValue
85
+ setValue(
86
+ parsedValue,
87
+ createChangeEventDetails<NumberFieldRootChangeEventReason, { direction?: Direction }>(
88
+ pressReason,
89
+ nativeEvent,
90
+ undefined,
91
+ { direction: isIncrement ? 1 : -1 },
92
+ ),
93
+ )
94
+ }
95
+ }
96
+
97
+ const { pointerHandlers, shouldSkipClick } = usePressAndHold({
98
+ disabled: () => disabled() || readOnly(),
99
+ elementRef: inputRef,
100
+ tickDelay: CHANGE_VALUE_TICK_DELAY,
101
+ startDelay: START_AUTO_CHANGE_DELAY,
102
+ scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
103
+ tick(triggerEvent) {
104
+ const amount = getStepAmount(triggerEvent as EventWithOptionalKeyState) ?? DEFAULT_STEP
105
+ return incrementValue(amount, {
106
+ direction: isIncrement ? 1 : -1,
107
+ event: triggerEvent,
108
+ reason: pressReason,
109
+ })
110
+ },
111
+ onStop(nativeEvent: PointerEvent) {
112
+ const committed = lastChangedValueRef.value ?? valueRef.value
113
+ onValueCommitted(committed, createGenericEventDetails(pressReason, nativeEvent))
114
+ },
115
+ })
116
+
117
+ return computed<Record<string, any>>(() => ({
118
+ 'disabled': disabled(),
119
+ 'aria-readonly': readOnly() || undefined,
120
+ 'aria-label': isIncrement ? 'Increase' : 'Decrease',
121
+ 'aria-controls': params.id(),
122
+ // Keyboard users shouldn't have access to the buttons, since they can use the input element
123
+ // to change the value. On the other hand, `aria-hidden` is not applied because touch screen
124
+ // readers should be able to use the buttons.
125
+ 'tabindex': -1,
126
+ 'style': {
127
+ WebkitUserSelect: 'none',
128
+ userSelect: 'none',
129
+ },
130
+ ...pointerHandlers,
131
+ onClick(event: MouseEvent) {
132
+ const isDisabled = disabled() || readOnly()
133
+ if (event.defaultPrevented || isDisabled || shouldSkipClick(event)) {
134
+ return
135
+ }
136
+
137
+ commitValue(event)
138
+
139
+ const amount = getStepAmount(event as EventWithOptionalKeyState) ?? DEFAULT_STEP
140
+
141
+ const prev = valueRef.value
142
+
143
+ incrementValue(amount, {
144
+ direction: isIncrement ? 1 : -1,
145
+ event,
146
+ reason: pressReason,
147
+ })
148
+
149
+ const committed = lastChangedValueRef.value ?? valueRef.value
150
+ if (committed !== prev) {
151
+ onValueCommitted(committed, createGenericEventDetails(pressReason, event))
152
+ }
153
+ },
154
+ onPointerdown(event: PointerEvent) {
155
+ const isMainButton = !event.button || event.button === 0
156
+ if (event.defaultPrevented || readOnly() || !isMainButton || disabled()) {
157
+ return
158
+ }
159
+
160
+ // Sync dirty input value before starting the hold sequence.
161
+ commitValue(event)
162
+
163
+ if (!isTouchLikePointerType(event.pointerType)) {
164
+ // Focus the input so the user can continue with keyboard interactions.
165
+ inputRef.value?.focus()
166
+ }
167
+
168
+ pointerHandlers.onPointerdown(event)
169
+ },
170
+ }))
171
+ }
@@ -0,0 +1,359 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import type { NumberFieldRootState } from '../root/NumberFieldRoot.vue'
4
+ import { computed, onMounted, provide, ref, useAttrs, watch } from 'vue'
5
+ import { getTarget } from '../../floating-ui-vue/utils'
6
+ import { mergeProps } from '../../merge-props/mergeProps'
7
+ import { createGenericEventDetails } from '../../utils/createBaseUIEventDetails'
8
+ import { isFirefox, isWebKit } from '../../utils/detectBrowser'
9
+ import { ownerDocument, ownerWindow } from '../../utils/owner'
10
+ import { REASONS } from '../../utils/reasons'
11
+ import { useRenderElement } from '../../utils/useRenderElement'
12
+ import { useTimeout } from '../../utils/useTimeout'
13
+ import { useNumberFieldRootContext } from '../root/NumberFieldRootContext'
14
+ import { DEFAULT_STEP } from '../utils/constants'
15
+ import { getViewportRect } from '../utils/getViewportRect'
16
+ import { stateAttributesMapping } from '../utils/stateAttributesMapping'
17
+ import { subscribeToVisualViewportResize } from '../utils/subscribeToVisualViewportResize'
18
+ import { numberFieldScrubAreaContextKey } from './NumberFieldScrubAreaContext'
19
+
20
+ export type NumberFieldScrubAreaState = NumberFieldRootState
21
+
22
+ export interface NumberFieldScrubAreaProps extends BaseUIComponentProps<NumberFieldScrubAreaState> {
23
+ /**
24
+ * Cursor movement direction in the scrub area.
25
+ * @default 'horizontal'
26
+ */
27
+ direction?: 'horizontal' | 'vertical'
28
+ /**
29
+ * Determines how many pixels the cursor must move before the value changes.
30
+ * A higher value will make scrubbing less sensitive.
31
+ * @default 2
32
+ */
33
+ pixelSensitivity?: number
34
+ /**
35
+ * If specified, determines the distance that the cursor may move from the center
36
+ * of the scrub area before it will loop back around.
37
+ */
38
+ teleportDistance?: number
39
+ }
40
+
41
+ defineOptions({
42
+ name: 'NumberFieldScrubArea',
43
+ inheritAttrs: false,
44
+ })
45
+
46
+ const props = withDefaults(defineProps<NumberFieldScrubAreaProps>(), {
47
+ as: 'span',
48
+ direction: 'horizontal',
49
+ pixelSensitivity: 2,
50
+ })
51
+
52
+ const attrs = useAttrs()
53
+ const attrsObject = attrs as Record<string, any>
54
+
55
+ const {
56
+ state,
57
+ setIsScrubbing: setRootScrubbing,
58
+ disabled,
59
+ readOnly,
60
+ inputRef,
61
+ incrementValue,
62
+ allowInputSyncRef,
63
+ getStepAmount,
64
+ onValueCommitted,
65
+ lastChangedValueRef,
66
+ valueRef,
67
+ } = useNumberFieldRootContext()
68
+
69
+ const direction = computed(() => props.direction)
70
+ const pixelSensitivity = computed(() => props.pixelSensitivity)
71
+ const teleportDistance = computed(() => props.teleportDistance)
72
+
73
+ const scrubAreaRef = ref<HTMLElement | null>(null)
74
+ const scrubAreaCursorRef = ref<HTMLElement | null>(null)
75
+
76
+ let isScrubbingInternal = false
77
+ let didMove = false
78
+ let pointerDownTarget: EventTarget | null = null
79
+ let virtualCursorCoords = { x: 0, y: 0 }
80
+ const visualScaleRef = ref(1)
81
+
82
+ const exitPointerLockTimeout = useTimeout()
83
+
84
+ const isTouchInput = ref(false)
85
+ const isPointerLockDenied = ref(false)
86
+ const isScrubbing = ref(false)
87
+
88
+ watch(
89
+ isScrubbing,
90
+ (scrubbing, _prev, onCleanup) => {
91
+ if (!scrubbing || !scrubAreaCursorRef.value) {
92
+ return
93
+ }
94
+
95
+ const unsubscribe = subscribeToVisualViewportResize(scrubAreaCursorRef.value, visualScaleRef)
96
+ onCleanup(unsubscribe)
97
+ },
98
+ { flush: 'post' },
99
+ )
100
+
101
+ function updateCursorTransform(x: number, y: number) {
102
+ if (scrubAreaCursorRef.value) {
103
+ scrubAreaCursorRef.value.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / visualScaleRef.value})`
104
+ }
105
+ }
106
+
107
+ function onScrub({ movementX, movementY }: PointerEvent) {
108
+ const virtualCursor = scrubAreaCursorRef.value
109
+ const scrubAreaEl = scrubAreaRef.value
110
+
111
+ if (!virtualCursor || !scrubAreaEl) {
112
+ return
113
+ }
114
+
115
+ const rect = getViewportRect(teleportDistance.value, scrubAreaEl)
116
+
117
+ const coords = virtualCursorCoords
118
+ const newCoords = {
119
+ x: Math.round(coords.x + movementX),
120
+ y: Math.round(coords.y + movementY),
121
+ }
122
+
123
+ const cursorWidth = virtualCursor.offsetWidth
124
+ const cursorHeight = virtualCursor.offsetHeight
125
+
126
+ if (newCoords.x + cursorWidth / 2 < rect.x) {
127
+ newCoords.x = rect.width - cursorWidth / 2
128
+ }
129
+ else if (newCoords.x + cursorWidth / 2 > rect.width) {
130
+ newCoords.x = rect.x - cursorWidth / 2
131
+ }
132
+
133
+ if (newCoords.y + cursorHeight / 2 < rect.y) {
134
+ newCoords.y = rect.height - cursorHeight / 2
135
+ }
136
+ else if (newCoords.y + cursorHeight / 2 > rect.height) {
137
+ newCoords.y = rect.y - cursorHeight / 2
138
+ }
139
+
140
+ virtualCursorCoords = newCoords
141
+
142
+ updateCursorTransform(newCoords.x, newCoords.y)
143
+ }
144
+
145
+ function onScrubbingChange(scrubbingValue: boolean, { clientX, clientY }: PointerEvent) {
146
+ isScrubbing.value = scrubbingValue
147
+ setRootScrubbing(scrubbingValue)
148
+
149
+ const virtualCursor = scrubAreaCursorRef.value
150
+ if (!virtualCursor || !scrubbingValue) {
151
+ return
152
+ }
153
+
154
+ const initialCoords = {
155
+ x: clientX - virtualCursor.offsetWidth / 2,
156
+ y: clientY - virtualCursor.offsetHeight / 2,
157
+ }
158
+
159
+ virtualCursorCoords = initialCoords
160
+
161
+ updateCursorTransform(initialCoords.x, initialCoords.y)
162
+ }
163
+
164
+ // Register global scrubbing listeners only while actively scrubbing.
165
+ watch(
166
+ [isScrubbing, disabled, readOnly],
167
+ (_value, _prev, onCleanup) => {
168
+ if (!inputRef.value || disabled.value || readOnly.value || !isScrubbing.value) {
169
+ return
170
+ }
171
+
172
+ let cumulativeDelta = 0
173
+
174
+ function handleScrubPointerUp(event: PointerEvent) {
175
+ function handler() {
176
+ try {
177
+ ownerDocument(scrubAreaRef.value)?.exitPointerLock()
178
+ }
179
+ catch {
180
+ // Ignore errors.
181
+ }
182
+ finally {
183
+ isScrubbingInternal = false
184
+ onScrubbingChange(false, event)
185
+ onValueCommitted(
186
+ lastChangedValueRef.value ?? valueRef.value,
187
+ createGenericEventDetails(REASONS.scrub, event),
188
+ )
189
+
190
+ // Manually dispatch a click event if no movement happened, since
191
+ // preventDefault on pointerdown prevents the browser click event.
192
+ const target = pointerDownTarget
193
+ const input = inputRef.value
194
+ if (!didMove && target != null && input) {
195
+ target.dispatchEvent(
196
+ new (ownerWindow(input).MouseEvent)('click', {
197
+ bubbles: true,
198
+ cancelable: true,
199
+ }),
200
+ )
201
+ }
202
+
203
+ didMove = false
204
+ pointerDownTarget = null
205
+ }
206
+ }
207
+
208
+ if (isFirefox) {
209
+ // Firefox needs a small delay here when soft-clicking as the pointer
210
+ // lock will not release otherwise.
211
+ exitPointerLockTimeout.start(20, handler)
212
+ }
213
+ else {
214
+ handler()
215
+ }
216
+ }
217
+
218
+ function handleScrubPointerMove(event: PointerEvent) {
219
+ if (!isScrubbingInternal) {
220
+ return
221
+ }
222
+
223
+ // Prevent text selection.
224
+ event.preventDefault()
225
+
226
+ onScrub(event)
227
+
228
+ const { movementX, movementY } = event
229
+
230
+ cumulativeDelta += direction.value === 'vertical' ? movementY : movementX
231
+
232
+ if (Math.abs(cumulativeDelta) >= pixelSensitivity.value) {
233
+ cumulativeDelta = 0
234
+ didMove = true
235
+ const dValue = direction.value === 'vertical' ? -movementY : movementX
236
+ const stepAmount = getStepAmount(event) ?? DEFAULT_STEP
237
+ const rawAmount = dValue * stepAmount
238
+
239
+ if (rawAmount !== 0) {
240
+ allowInputSyncRef.value = true
241
+ incrementValue(Math.abs(rawAmount), {
242
+ direction: rawAmount >= 0 ? 1 : -1,
243
+ event,
244
+ reason: REASONS.scrub,
245
+ })
246
+ }
247
+ }
248
+ }
249
+
250
+ const win = ownerWindow(inputRef.value)
251
+ win.addEventListener('pointerup', handleScrubPointerUp, true)
252
+ win.addEventListener('pointermove', handleScrubPointerMove, true)
253
+
254
+ onCleanup(() => {
255
+ exitPointerLockTimeout.clear()
256
+ win.removeEventListener('pointerup', handleScrubPointerUp, true)
257
+ win.removeEventListener('pointermove', handleScrubPointerMove, true)
258
+ })
259
+ },
260
+ { flush: 'post' },
261
+ )
262
+
263
+ // Prevent scrolling using touch input when scrubbing.
264
+ onMounted(() => {
265
+ const element = scrubAreaRef.value
266
+ if (!element || disabled.value || readOnly.value) {
267
+ return
268
+ }
269
+
270
+ function handleTouchStart(event: TouchEvent) {
271
+ if (event.touches.length === 1) {
272
+ event.preventDefault()
273
+ }
274
+ }
275
+
276
+ element.addEventListener('touchstart', handleTouchStart, { passive: false })
277
+ })
278
+
279
+ async function onPointerdown(event: PointerEvent) {
280
+ const isMainButton = !event.button || event.button === 0
281
+ if (event.defaultPrevented || readOnly.value || !isMainButton || disabled.value) {
282
+ return
283
+ }
284
+
285
+ const isTouch = event.pointerType === 'touch'
286
+ isTouchInput.value = isTouch
287
+
288
+ if (event.pointerType === 'mouse') {
289
+ event.preventDefault()
290
+ inputRef.value?.focus()
291
+ }
292
+
293
+ isScrubbingInternal = true
294
+ didMove = false
295
+ pointerDownTarget = getTarget(event)
296
+ onScrubbingChange(true, event)
297
+
298
+ // WebKit causes significant layout shift with the native message, so we can't use it.
299
+ if (!isTouch && !isWebKit) {
300
+ try {
301
+ // Avoid non-deterministic errors in testing environments.
302
+ await ownerDocument(scrubAreaRef.value)?.body.requestPointerLock()
303
+ isPointerLockDenied.value = false
304
+ }
305
+ catch {
306
+ isPointerLockDenied.value = true
307
+ }
308
+ finally {
309
+ if (isScrubbingInternal) {
310
+ onScrubbingChange(true, event)
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ const defaultProps = computed(() => ({
317
+ role: 'presentation',
318
+ style: {
319
+ touchAction: 'none',
320
+ WebkitUserSelect: 'none',
321
+ userSelect: 'none',
322
+ },
323
+ onPointerdown,
324
+ }))
325
+
326
+ const scrubAreaProps = computed(() => mergeProps(defaultProps.value, attrsObject))
327
+
328
+ provide(numberFieldScrubAreaContextKey, {
329
+ isScrubbing,
330
+ isTouchInput,
331
+ isPointerLockDenied,
332
+ scrubAreaCursorRef,
333
+ scrubAreaRef,
334
+ direction,
335
+ pixelSensitivity,
336
+ teleportDistance,
337
+ })
338
+
339
+ const {
340
+ tag,
341
+ mergedProps,
342
+ renderless,
343
+ ref: renderRef,
344
+ } = useRenderElement({
345
+ componentProps: props,
346
+ state,
347
+ props: scrubAreaProps,
348
+ stateAttributesMapping,
349
+ defaultTagName: 'span',
350
+ ref: scrubAreaRef,
351
+ })
352
+ </script>
353
+
354
+ <template>
355
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
356
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
357
+ <slot :state="state" />
358
+ </component>
359
+ </template>
@@ -0,0 +1,26 @@
1
+ import type { ComputedRef, InjectionKey, Ref } from 'vue'
2
+ import { inject } from 'vue'
3
+
4
+ export interface NumberFieldScrubAreaContext {
5
+ isScrubbing: Readonly<Ref<boolean>>
6
+ isTouchInput: Readonly<Ref<boolean>>
7
+ isPointerLockDenied: Readonly<Ref<boolean>>
8
+ scrubAreaCursorRef: Ref<HTMLElement | null>
9
+ scrubAreaRef: Ref<HTMLElement | null>
10
+ direction: ComputedRef<'horizontal' | 'vertical'>
11
+ pixelSensitivity: ComputedRef<number>
12
+ teleportDistance: ComputedRef<number | undefined>
13
+ }
14
+
15
+ export const numberFieldScrubAreaContextKey: InjectionKey<NumberFieldScrubAreaContext>
16
+ = Symbol('NumberFieldScrubAreaContext')
17
+
18
+ export function useNumberFieldScrubAreaContext() {
19
+ const context = inject(numberFieldScrubAreaContextKey, undefined)
20
+ if (context === undefined) {
21
+ throw new Error(
22
+ 'Base UI Vue: NumberFieldScrubAreaContext is missing. NumberFieldScrubArea parts must be placed within <NumberFieldScrubArea>.',
23
+ )
24
+ }
25
+ return context
26
+ }
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ import type { ComponentPublicInstance } from 'vue'
3
+ import type { BaseUIComponentProps } from '../../utils/types'
4
+ import type { NumberFieldRootState } from '../root/NumberFieldRoot.vue'
5
+ import { computed, useAttrs } from 'vue'
6
+ import { mergeProps } from '../../merge-props/mergeProps'
7
+ import { isWebKit } from '../../utils/detectBrowser'
8
+ import { useRenderElement } from '../../utils/useRenderElement'
9
+ import { useNumberFieldRootContext } from '../root/NumberFieldRootContext'
10
+ import { useNumberFieldScrubAreaContext } from '../scrub-area/NumberFieldScrubAreaContext'
11
+ import { stateAttributesMapping } from '../utils/stateAttributesMapping'
12
+
13
+ export type NumberFieldScrubAreaCursorState = NumberFieldRootState
14
+
15
+ export interface NumberFieldScrubAreaCursorProps
16
+ extends BaseUIComponentProps<NumberFieldScrubAreaCursorState> {}
17
+
18
+ defineOptions({
19
+ name: 'NumberFieldScrubAreaCursor',
20
+ inheritAttrs: false,
21
+ })
22
+
23
+ const props = defineProps<NumberFieldScrubAreaCursorProps>()
24
+
25
+ const attrs = useAttrs()
26
+ const attrsObject = attrs as Record<string, any>
27
+
28
+ const { state } = useNumberFieldRootContext()
29
+ const { isScrubbing, isTouchInput, isPointerLockDenied, scrubAreaCursorRef }
30
+ = useNumberFieldScrubAreaContext()
31
+
32
+ const shouldRender = computed(
33
+ () => isScrubbing.value && !isWebKit && !isTouchInput.value && !isPointerLockDenied.value,
34
+ )
35
+
36
+ function setCursorRef(el: Element | ComponentPublicInstance | null) {
37
+ scrubAreaCursorRef.value = el as HTMLElement | null
38
+ }
39
+
40
+ const cursorProps = computed(() => mergeProps(
41
+ {
42
+ role: 'presentation',
43
+ style: {
44
+ position: 'fixed',
45
+ top: 0,
46
+ left: 0,
47
+ pointerEvents: 'none',
48
+ },
49
+ },
50
+ attrsObject,
51
+ ))
52
+
53
+ const {
54
+ tag,
55
+ mergedProps,
56
+ renderless,
57
+ ref: renderRef,
58
+ } = useRenderElement({
59
+ componentProps: props,
60
+ state,
61
+ props: cursorProps,
62
+ stateAttributesMapping,
63
+ defaultTagName: 'span',
64
+ ref: setCursorRef,
65
+ })
66
+ </script>
67
+
68
+ <template>
69
+ <Teleport v-if="shouldRender" to="body">
70
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
71
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
72
+ <slot :state="state" />
73
+ </component>
74
+ </Teleport>
75
+ </template>
@@ -0,0 +1,4 @@
1
+ export const CHANGE_VALUE_TICK_DELAY = 60
2
+ export const START_AUTO_CHANGE_DELAY = 400
3
+ export const SCROLLING_POINTER_MOVE_DISTANCE = 8
4
+ export const DEFAULT_STEP = 1
@@ -0,0 +1,34 @@
1
+ import { ownerWindow } from '../../utils/owner'
2
+
3
+ // Calculates the viewport rect for the virtual cursor.
4
+ export function getViewportRect(teleportDistance: number | undefined, scrubAreaEl: HTMLElement) {
5
+ const win = ownerWindow(scrubAreaEl)
6
+ const rect = scrubAreaEl.getBoundingClientRect()
7
+
8
+ if (rect && teleportDistance != null) {
9
+ return {
10
+ x: rect.left - teleportDistance / 2,
11
+ y: rect.top - teleportDistance / 2,
12
+ width: rect.right + teleportDistance / 2,
13
+ height: rect.bottom + teleportDistance / 2,
14
+ }
15
+ }
16
+
17
+ const vV = win.visualViewport
18
+
19
+ if (vV) {
20
+ return {
21
+ x: vV.offsetLeft,
22
+ y: vV.offsetTop,
23
+ width: vV.offsetLeft + vV.width,
24
+ height: vV.offsetTop + vV.height,
25
+ }
26
+ }
27
+
28
+ return {
29
+ x: 0,
30
+ y: 0,
31
+ width: win.document.documentElement.clientWidth,
32
+ height: win.document.documentElement.clientHeight,
33
+ }
34
+ }