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,583 @@
1
+ <script setup lang="ts">
2
+ import type { FieldRootState } from '../../field/root/FieldRoot.vue'
3
+ import type { BaseUIComponentProps } from '../../utils/types'
4
+ import type { OtpValidationType } from '../utils/otp'
5
+ import type {
6
+ OtpFieldRootChangeEventDetails,
7
+ OtpFieldRootCompleteEventDetails,
8
+ OtpFieldRootInvalidEventDetails,
9
+ } from './OtpFieldRootContext'
10
+ import { computed, provide, ref, useAttrs, watchEffect } from 'vue'
11
+ import CompositeList from '../../composite/list/CompositeList.vue'
12
+ import { useFieldRootContext } from '../../field/root/FieldRootContext'
13
+ import { useField } from '../../field/useField'
14
+ import { contains } from '../../floating-ui-vue/utils'
15
+ import { useFormContext } from '../../form/FormContext'
16
+ import { useLabelableContext } from '../../labelable-provider/LabelableContext'
17
+ import { useAriaLabelledBy } from '../../labelable-provider/useAriaLabelledBy'
18
+ import { useLabelableId } from '../../labelable-provider/useLabelableId'
19
+ import { mergeProps } from '../../merge-props/mergeProps'
20
+ import { createChangeEventDetails, createGenericEventDetails } from '../../utils/createBaseUIEventDetails'
21
+ import { ownerDocument } from '../../utils/owner'
22
+ import { REASONS } from '../../utils/reasons'
23
+ import { useControllableState } from '../../utils/useControllableState'
24
+ import { useRenderElement } from '../../utils/useRenderElement'
25
+ import { useValueChanged } from '../../utils/useValueChanged'
26
+ import { visuallyHidden, visuallyHiddenInput } from '../../utils/visuallyHidden'
27
+ import { warn } from '../../utils/warn'
28
+ import { getOTPValidationConfig, getOTPValueLength, normalizeOTPValue, normalizeOTPValueWithDetails } from '../utils/otp'
29
+ import { rootStateAttributesMapping } from '../utils/stateAttributesMapping'
30
+ import { otpFieldRootContextKey } from './OtpFieldRootContext'
31
+
32
+ export interface OtpFieldRootState extends FieldRootState {
33
+ /**
34
+ * Whether all slots are filled.
35
+ */
36
+ complete: boolean
37
+ /**
38
+ * Whether the component should ignore user interaction.
39
+ */
40
+ disabled: boolean
41
+ /**
42
+ * The number of OTP input slots.
43
+ */
44
+ length: number
45
+ /**
46
+ * Whether the user should be unable to change the field value.
47
+ */
48
+ readOnly: boolean
49
+ /**
50
+ * Whether the user must enter a value before submitting a form.
51
+ */
52
+ required: boolean
53
+ /**
54
+ * The OTP value.
55
+ */
56
+ value: string
57
+ }
58
+
59
+ export interface OtpFieldRootProps extends BaseUIComponentProps<OtpFieldRootState> {
60
+ /**
61
+ * The id of the first input element.
62
+ * Subsequent inputs derive their ids from it (`{id}-2`, `{id}-3`, and so on).
63
+ */
64
+ id?: string
65
+ /**
66
+ * The input autocomplete attribute. Applied to the first slot and hidden validation input.
67
+ * @default 'one-time-code'
68
+ */
69
+ autoComplete?: string
70
+ /**
71
+ * A string specifying the `form` element with which the hidden input is associated.
72
+ * This string's value must match the id of a `form` element in the same document.
73
+ */
74
+ form?: string
75
+ /**
76
+ * The number of OTP input slots.
77
+ * Required so the root can clamp values, detect completion, and generate
78
+ * consistent validation markup before all slots hydrate.
79
+ */
80
+ length: number
81
+ /**
82
+ * Whether to submit the owning form when the OTP becomes complete.
83
+ * @default false
84
+ */
85
+ autoSubmit?: boolean
86
+ /**
87
+ * Whether the slot inputs should mask entered characters.
88
+ * Pass `type` directly to individual `OtpFieldInput` parts to use a custom input type.
89
+ * @default false
90
+ */
91
+ mask?: boolean
92
+ /**
93
+ * The virtual keyboard hint applied to the slot inputs and hidden validation input.
94
+ */
95
+ inputMode?: string
96
+ /**
97
+ * The type of input validation to apply to the OTP value.
98
+ * @default 'numeric'
99
+ */
100
+ validationType?: OtpValidationType
101
+ /**
102
+ * Function that normalizes the OTP value after whitespace and `validationType` filtering.
103
+ * It should be idempotent because OtpField may normalize the same value more than once.
104
+ */
105
+ normalizeValue?: (value: string) => string
106
+ /**
107
+ * Whether the user must enter a value before submitting a form.
108
+ * @default false
109
+ */
110
+ required?: boolean
111
+ /**
112
+ * Whether the component should ignore user interaction.
113
+ * @default false
114
+ */
115
+ disabled?: boolean
116
+ /**
117
+ * Whether the user should be unable to change the field value.
118
+ * @default false
119
+ */
120
+ readOnly?: boolean
121
+ /**
122
+ * Identifies the field when a form is submitted.
123
+ */
124
+ name?: string
125
+ /**
126
+ * The OTP value.
127
+ */
128
+ value?: string
129
+ /**
130
+ * The uncontrolled OTP value when the component is initially rendered.
131
+ */
132
+ defaultValue?: string
133
+ }
134
+
135
+ defineOptions({
136
+ name: 'OtpFieldRoot',
137
+ inheritAttrs: false,
138
+ })
139
+
140
+ const props = withDefaults(defineProps<OtpFieldRootProps>(), {
141
+ as: 'div',
142
+ autoComplete: 'one-time-code',
143
+ autoSubmit: false,
144
+ mask: false,
145
+ validationType: 'numeric',
146
+ required: false,
147
+ disabled: false,
148
+ readOnly: false,
149
+ })
150
+
151
+ const emit = defineEmits<{
152
+ /**
153
+ * Fired when the OTP value changes.
154
+ */
155
+ valueChange: [value: string, eventDetails: OtpFieldRootChangeEventDetails]
156
+ /**
157
+ * Fired when entered text contains characters rejected by validation or normalization.
158
+ */
159
+ valueInvalid: [value: string, eventDetails: OtpFieldRootInvalidEventDetails]
160
+ /**
161
+ * Fired when the OTP value becomes complete.
162
+ */
163
+ valueComplete: [value: string, eventDetails: OtpFieldRootCompleteEventDetails]
164
+ }>()
165
+
166
+ const attrs = useAttrs()
167
+ const attrsObject = attrs as Record<string, any>
168
+
169
+ const {
170
+ setDirty,
171
+ validityData,
172
+ disabled: fieldDisabled,
173
+ setFilled,
174
+ invalid,
175
+ name: fieldName,
176
+ state: fieldState,
177
+ validation,
178
+ validationMode,
179
+ setFocused,
180
+ setTouched,
181
+ } = useFieldRootContext()
182
+ const { clearErrors } = useFormContext()
183
+ const { getDescriptionProps, labelId } = useLabelableContext()
184
+
185
+ const length = computed(() => props.length)
186
+ const validationType = computed(() => props.validationType)
187
+ const normalizeValueProp = computed(() => props.normalizeValue)
188
+ const disabled = computed(() => fieldDisabled.value || props.disabled)
189
+ const readOnly = computed(() => props.readOnly)
190
+ const required = computed(() => props.required)
191
+ const mask = computed(() => props.mask)
192
+ const autoComplete = computed(() => props.autoComplete)
193
+ const formProp = computed(() => props.form)
194
+
195
+ const name = computed(() => fieldName.value ?? props.name)
196
+
197
+ const { value: valueUnwrapped, setValue: setValueUnwrapped } = useControllableState<string>({
198
+ controlled: () => props.value,
199
+ default: () => props.defaultValue ?? '',
200
+ name: 'OtpField',
201
+ state: 'value',
202
+ })
203
+
204
+ const value = computed(() =>
205
+ normalizeOTPValue(valueUnwrapped.value, length.value, validationType.value, normalizeValueProp.value),
206
+ )
207
+ const valueLength = computed(() => getOTPValueLength(value.value))
208
+ const filled = computed(() => value.value !== '')
209
+
210
+ const rootRef = ref<HTMLElement | null>(null)
211
+ // Wrap in a plain object so the template binding passes the Ref itself instead of
212
+ // the auto-unwrapped array (CompositeList needs the live Ref).
213
+ const inputRefsHolder = { elementsRef: ref<Array<HTMLElement | null>>([]) }
214
+ const inputRefs = inputRefsHolder.elementsRef
215
+ const validationInputRef = ref<HTMLInputElement | null>(null)
216
+ const firstInputRef = {
217
+ get value() {
218
+ return inputRefs.value[0] ?? null
219
+ },
220
+ }
221
+
222
+ let pendingFocus: { index: number, value: string } | null = null
223
+ let pendingCompleteValue: { value: string, eventDetails: OtpFieldRootCompleteEventDetails } | null = null
224
+
225
+ const inputCount = ref(0)
226
+ const focusedIndex = ref(Math.min(valueLength.value, length.value - 1))
227
+ const focused = ref(false)
228
+
229
+ const id = useLabelableId({ id: computed(() => props.id) })
230
+
231
+ const ariaLabelledByAttr = computed(() => attrs['aria-labelledby'] as string | undefined)
232
+ const ariaLabelledBy = useAriaLabelledBy({
233
+ ariaLabelledBy: ariaLabelledByAttr,
234
+ labelId,
235
+ labelSourceRef: firstInputRef,
236
+ enableFallback: true,
237
+ labelSourceId: () => id.value ?? undefined,
238
+ })
239
+ const inputAriaLabelledBy = computed(() =>
240
+ ariaLabelledByAttr.value == null ? ariaLabelledBy.value : undefined,
241
+ )
242
+
243
+ const ariaDescribedBy = computed(() =>
244
+ mergeAriaIds(
245
+ attrs['aria-describedby'] as string | undefined,
246
+ getDescriptionProps()['aria-describedby'],
247
+ ),
248
+ )
249
+
250
+ const validationConfig = computed(() => getOTPValidationConfig(validationType.value))
251
+ const pattern = computed(() => validationConfig.value?.slotPattern)
252
+ const hiddenInputPattern = computed(() => validationConfig.value?.getRootPattern(length.value))
253
+ const inputMode = computed(() => props.inputMode ?? validationConfig.value?.inputMode)
254
+ const hasValidLength = computed(() => Number.isInteger(length.value) && length.value > 0)
255
+
256
+ const activeIndex = computed(() =>
257
+ focused.value
258
+ ? Math.min(focusedIndex.value, Math.max(length.value - 1, 0))
259
+ : Math.min(valueLength.value, length.value - 1),
260
+ )
261
+
262
+ watchEffect(() => {
263
+ setFilled(filled.value)
264
+ })
265
+
266
+ watchEffect(() => {
267
+ validation.setInputRef(validationInputRef.value)
268
+ })
269
+
270
+ useField({
271
+ enabled: computed(() => !disabled.value),
272
+ id,
273
+ name,
274
+ commit: (v: unknown) => validation.commit(v),
275
+ value,
276
+ getValue: () => value.value,
277
+ controlRef: firstInputRef,
278
+ })
279
+
280
+ function focusInput(index: number) {
281
+ const targetIndex = Math.min(Math.max(index, 0), Math.max(inputRefs.value.length - 1, 0))
282
+ const target = inputRefs.value[targetIndex] as HTMLInputElement | null
283
+ target?.focus()
284
+ target?.select()
285
+ }
286
+
287
+ function queueFocusInput(index: number, nextValue: string) {
288
+ pendingFocus = { index, value: nextValue }
289
+ }
290
+
291
+ function requestSubmit() {
292
+ let formElement: HTMLFormElement | null
293
+ = validationInputRef.value?.form
294
+ ?? (inputRefs.value[0] as HTMLInputElement | null)?.form
295
+ ?? null
296
+
297
+ if (formProp.value) {
298
+ const associatedElement = ownerDocument(rootRef.value)?.getElementById(formProp.value)
299
+ if (associatedElement?.tagName === 'FORM') {
300
+ formElement = associatedElement as HTMLFormElement
301
+ }
302
+ }
303
+
304
+ if (formElement && typeof formElement.requestSubmit === 'function') {
305
+ formElement.requestSubmit()
306
+ }
307
+ }
308
+
309
+ function completeValue(completedValue: string, eventDetails: OtpFieldRootCompleteEventDetails) {
310
+ emit('valueComplete', completedValue, eventDetails)
311
+
312
+ if (props.autoSubmit) {
313
+ requestSubmit()
314
+ }
315
+ }
316
+
317
+ function getCompleteEventDetails(details: OtpFieldRootChangeEventDetails) {
318
+ if (details.reason === REASONS.inputChange || details.reason === REASONS.inputPaste) {
319
+ return createGenericEventDetails(details.reason, details.event) as OtpFieldRootCompleteEventDetails
320
+ }
321
+
322
+ return null
323
+ }
324
+
325
+ function setValue(nextValue: string, details: OtpFieldRootChangeEventDetails): string | null {
326
+ const currentValue = value.value
327
+ const normalizedValue = normalizeOTPValue(
328
+ nextValue,
329
+ length.value,
330
+ validationType.value,
331
+ normalizeValueProp.value,
332
+ )
333
+ const normalizedValueLength = getOTPValueLength(normalizedValue)
334
+ const completeEventDetails
335
+ = normalizedValueLength === length.value
336
+ && (getOTPValueLength(currentValue) !== length.value || details.reason === REASONS.inputPaste)
337
+ ? getCompleteEventDetails(details)
338
+ : null
339
+
340
+ if (normalizedValue === currentValue) {
341
+ if (completeEventDetails != null) {
342
+ completeValue(normalizedValue, completeEventDetails)
343
+ }
344
+
345
+ return null
346
+ }
347
+
348
+ emit('valueChange', normalizedValue, details)
349
+
350
+ if (details.isCanceled) {
351
+ return null
352
+ }
353
+
354
+ setValueUnwrapped(normalizedValue)
355
+
356
+ if (completeEventDetails != null) {
357
+ pendingCompleteValue = { value: normalizedValue, eventDetails: completeEventDetails }
358
+ }
359
+ else if (normalizedValueLength !== length.value) {
360
+ pendingCompleteValue = null
361
+ }
362
+
363
+ return normalizedValue
364
+ }
365
+
366
+ function reportValueInvalid(invalidValue: string, details: OtpFieldRootInvalidEventDetails) {
367
+ emit('valueInvalid', invalidValue, details)
368
+ }
369
+
370
+ function handleInputFocus(index: number, event: FocusEvent) {
371
+ if (index > valueLength.value) {
372
+ focusInput(Math.min(valueLength.value, length.value - 1))
373
+ return
374
+ }
375
+
376
+ focusedIndex.value = index
377
+ focused.value = true
378
+ setFocused(true)
379
+ ;(event.currentTarget as HTMLInputElement).select()
380
+ }
381
+
382
+ function handleInputBlur(event: FocusEvent) {
383
+ if (contains(rootRef.value, event.relatedTarget as Element | null)) {
384
+ return
385
+ }
386
+
387
+ setTouched(true)
388
+ focused.value = false
389
+ setFocused(false)
390
+
391
+ if (validationMode.value === 'onBlur') {
392
+ validation.commit(value.value)
393
+ }
394
+ }
395
+
396
+ function getInputId(index: number) {
397
+ if (id.value == null) {
398
+ return undefined
399
+ }
400
+
401
+ return index === 0 ? id.value : `${id.value}-${index + 1}`
402
+ }
403
+
404
+ useValueChanged(value, () => {
405
+ clearErrors(name.value)
406
+ setDirty(value.value !== validityData.value.initialValue)
407
+
408
+ void validation.commit(value.value, true)
409
+
410
+ if (pendingCompleteValue != null) {
411
+ const pending = pendingCompleteValue
412
+ pendingCompleteValue = null
413
+
414
+ if (pending.value === value.value) {
415
+ completeValue(value.value, pending.eventDetails)
416
+ }
417
+ }
418
+
419
+ if (pendingFocus != null) {
420
+ const pending = pendingFocus
421
+ pendingFocus = null
422
+
423
+ if (pending.value === value.value) {
424
+ focusInput(pending.index)
425
+ }
426
+ }
427
+ })
428
+
429
+ const state = computed<OtpFieldRootState>(() => ({
430
+ ...fieldState.value,
431
+ complete: valueLength.value === length.value,
432
+ disabled: disabled.value,
433
+ filled: filled.value,
434
+ focused: focused.value,
435
+ length: length.value,
436
+ readOnly: readOnly.value,
437
+ required: required.value,
438
+ value: value.value,
439
+ }))
440
+
441
+ provide(otpFieldRootContextKey, {
442
+ activeIndex,
443
+ autoComplete,
444
+ disabled,
445
+ form: formProp,
446
+ focusInput,
447
+ queueFocusInput,
448
+ getInputId,
449
+ handleInputBlur,
450
+ handleInputFocus,
451
+ inputMode,
452
+ inputAriaLabelledBy,
453
+ invalid,
454
+ length,
455
+ mask,
456
+ pattern,
457
+ reportValueInvalid,
458
+ readOnly,
459
+ required,
460
+ normalizeValue: normalizeValueProp,
461
+ setValue,
462
+ state,
463
+ validationType,
464
+ value,
465
+ })
466
+
467
+ const rootProps = computed(() => mergeProps(
468
+ attrsObject,
469
+ {
470
+ 'role': 'group',
471
+ 'aria-describedby': ariaDescribedBy.value,
472
+ 'aria-labelledby': ariaLabelledBy.value,
473
+ },
474
+ ))
475
+
476
+ const {
477
+ tag,
478
+ mergedProps,
479
+ renderless,
480
+ ref: renderRef,
481
+ } = useRenderElement({
482
+ componentProps: props,
483
+ state,
484
+ props: rootProps,
485
+ stateAttributesMapping: rootStateAttributesMapping,
486
+ defaultTagName: 'div',
487
+ ref: rootRef,
488
+ })
489
+
490
+ const hiddenInputProps = computed<Record<string, any>>(() => ({
491
+ ...validation.getValidationProps(),
492
+ 'type': 'text',
493
+ 'id': id.value && name.value == null ? `${id.value}-hidden-input` : undefined,
494
+ 'form': formProp.value,
495
+ 'name': name.value,
496
+ 'value': value.value,
497
+ 'autocomplete': autoComplete.value,
498
+ 'inputmode': inputMode.value,
499
+ 'minlength': length.value,
500
+ 'maxlength': length.value,
501
+ 'pattern': hiddenInputPattern.value,
502
+ 'disabled': disabled.value,
503
+ 'readonly': readOnly.value,
504
+ 'required': required.value,
505
+ 'aria-hidden': true,
506
+ 'tabindex': -1,
507
+ 'style': name.value ? visuallyHiddenInput : visuallyHidden,
508
+ }))
509
+
510
+ function handleHiddenFocus() {
511
+ focusInput(0)
512
+ }
513
+
514
+ function handleHiddenInput(event: Event) {
515
+ const target = event.target as HTMLInputElement
516
+ if (event.defaultPrevented || disabled.value || readOnly.value) {
517
+ return
518
+ }
519
+
520
+ const rawValue = target.value
521
+ const [normalizedValue, didRejectCharacters] = normalizeOTPValueWithDetails(
522
+ rawValue,
523
+ length.value,
524
+ validationType.value,
525
+ normalizeValueProp.value,
526
+ )
527
+
528
+ if (didRejectCharacters) {
529
+ reportValueInvalid(rawValue, createGenericEventDetails(REASONS.inputChange, event))
530
+ }
531
+
532
+ const committedValue = setValue(
533
+ normalizedValue,
534
+ createChangeEventDetails(REASONS.inputChange, event),
535
+ )
536
+
537
+ if (committedValue != null && committedValue !== '') {
538
+ queueFocusInput(getOTPValueLength(committedValue) - 1, committedValue)
539
+ }
540
+ }
541
+
542
+ function mergeAriaIds(...values: Array<string | undefined>) {
543
+ const ids = values.flatMap(v => v?.split(/\s+/).filter(Boolean) ?? [])
544
+ return ids.length > 0 ? Array.from(new Set(ids)).join(' ') : undefined
545
+ }
546
+
547
+ if (process.env.NODE_ENV !== 'production') {
548
+ watchEffect(() => {
549
+ const len = length.value
550
+ if (!Number.isInteger(len) || len <= 0) {
551
+ warn(`<OtpFieldRoot> \`length\` must be a positive integer. Received \`length={${String(len)}}\`.`)
552
+ return
553
+ }
554
+
555
+ if (inputCount.value !== 0 && inputCount.value !== len) {
556
+ warn(
557
+ `<OtpFieldRoot> \`length\` must match the number of rendered <OtpFieldInput /> parts. `
558
+ + `Received \`length={${len}}\` but rendered ${inputCount.value} input${inputCount.value === 1 ? '' : 's'}.`,
559
+ )
560
+ }
561
+ })
562
+ }
563
+
564
+ function handleMapChange(map: Map<Element, unknown>) {
565
+ inputCount.value = map.size
566
+ }
567
+ </script>
568
+
569
+ <template>
570
+ <CompositeList :elements-ref="inputRefsHolder.elementsRef" :on-map-change="handleMapChange">
571
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
572
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
573
+ <slot :state="state" />
574
+ </component>
575
+ <input
576
+ v-if="hasValidLength"
577
+ ref="validationInputRef"
578
+ v-bind="hiddenInputProps"
579
+ @input="handleHiddenInput"
580
+ @focus="handleHiddenFocus"
581
+ >
582
+ </CompositeList>
583
+ </template>
@@ -0,0 +1,81 @@
1
+ import type { ComputedRef, InjectionKey, Ref } from 'vue'
2
+ import type {
3
+ BaseUIChangeEventDetails,
4
+ BaseUIGenericEventDetails,
5
+ } from '../../utils/createBaseUIEventDetails'
6
+ import type { REASONS } from '../../utils/reasons'
7
+ import type { OtpFieldInputState } from '../input/OtpFieldInput.vue'
8
+ import type { OtpValidationType } from '../utils/otp'
9
+ import type { OtpFieldRootState } from './OtpFieldRoot.vue'
10
+ import { inject } from 'vue'
11
+
12
+ export type OtpFieldRootChangeEventReason
13
+ = | typeof REASONS.inputChange
14
+ | typeof REASONS.inputClear
15
+ | typeof REASONS.inputPaste
16
+ | typeof REASONS.keyboard
17
+ export type OtpFieldRootChangeEventDetails
18
+ = BaseUIChangeEventDetails<OtpFieldRootChangeEventReason>
19
+
20
+ export type OtpFieldRootInvalidEventReason = typeof REASONS.inputChange | typeof REASONS.inputPaste
21
+ export type OtpFieldRootInvalidEventDetails
22
+ = BaseUIGenericEventDetails<OtpFieldRootInvalidEventReason>
23
+
24
+ export type OtpFieldRootCompleteEventReason
25
+ = | typeof REASONS.inputChange
26
+ | typeof REASONS.inputPaste
27
+ export type OtpFieldRootCompleteEventDetails
28
+ = BaseUIGenericEventDetails<OtpFieldRootCompleteEventReason>
29
+
30
+ export interface OtpFieldRootContext {
31
+ activeIndex: ComputedRef<number>
32
+ autoComplete: ComputedRef<string | undefined>
33
+ disabled: ComputedRef<boolean>
34
+ form: ComputedRef<string | undefined>
35
+ focusInput: (index: number) => void
36
+ queueFocusInput: (index: number, value: string) => void
37
+ getInputId: (index: number) => string | undefined
38
+ handleInputBlur: (event: FocusEvent) => void
39
+ handleInputFocus: (index: number, event: FocusEvent) => void
40
+ inputMode: ComputedRef<string | undefined>
41
+ inputAriaLabelledBy: ComputedRef<string | undefined>
42
+ invalid: Readonly<Ref<boolean | undefined>>
43
+ length: ComputedRef<number>
44
+ mask: ComputedRef<boolean>
45
+ pattern: ComputedRef<string | undefined>
46
+ reportValueInvalid: (value: string, details: OtpFieldRootInvalidEventDetails) => void
47
+ readOnly: ComputedRef<boolean>
48
+ required: ComputedRef<boolean>
49
+ normalizeValue: ComputedRef<((value: string) => string) | undefined>
50
+ setValue: (value: string, details: OtpFieldRootChangeEventDetails) => string | null
51
+ state: ComputedRef<OtpFieldRootState>
52
+ validationType: ComputedRef<OtpValidationType>
53
+ value: ComputedRef<string>
54
+ }
55
+
56
+ export const otpFieldRootContextKey: InjectionKey<OtpFieldRootContext> = Symbol('OtpFieldRootContext')
57
+
58
+ export function useOtpFieldRootContext() {
59
+ const context = inject(otpFieldRootContextKey, undefined)
60
+
61
+ if (context === undefined) {
62
+ throw new Error(
63
+ 'Base UI Vue: OtpFieldRootContext is missing. OtpField parts must be placed within <OtpFieldRoot>.',
64
+ )
65
+ }
66
+
67
+ return context
68
+ }
69
+
70
+ export function getOtpFieldInputState(
71
+ state: OtpFieldRootState,
72
+ value: string,
73
+ index: number,
74
+ ): OtpFieldInputState {
75
+ return {
76
+ ...state,
77
+ value,
78
+ index,
79
+ filled: value !== '',
80
+ }
81
+ }