base-ui-vue 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/arrow/TooltipArrow.cjs +609 -0
  2. package/dist/arrow/TooltipArrow.cjs.map +1 -0
  3. package/dist/arrow/TooltipArrow.js +478 -0
  4. package/dist/arrow/TooltipArrow.js.map +1 -0
  5. package/dist/button/Button.cjs +5 -317
  6. package/dist/button/Button.cjs.map +1 -1
  7. package/dist/button/Button.js +3 -249
  8. package/dist/button/Button.js.map +1 -1
  9. package/dist/button/ToolbarButton.cjs +19 -6
  10. package/dist/button/ToolbarButton.cjs.map +1 -1
  11. package/dist/button/ToolbarButton.js +3 -2
  12. package/dist/button/ToolbarButton.js.map +1 -1
  13. package/dist/checkbox/index.cjs +20 -9
  14. package/dist/checkbox/index.cjs.map +1 -1
  15. package/dist/checkbox/index.js +7 -2
  16. package/dist/checkbox/index.js.map +1 -1
  17. package/dist/checkbox-group/CheckboxGroup.cjs +14 -13
  18. package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -1
  19. package/dist/checkbox-group/CheckboxGroup.js +2 -1
  20. package/dist/checkbox-group/CheckboxGroup.js.map +1 -1
  21. package/dist/components/FloatingPortal.cjs +66 -0
  22. package/dist/components/FloatingPortal.cjs.map +1 -0
  23. package/dist/components/FloatingPortal.js +55 -0
  24. package/dist/components/FloatingPortal.js.map +1 -0
  25. package/dist/components/FloatingRootStore.cjs +101 -0
  26. package/dist/components/FloatingRootStore.cjs.map +1 -0
  27. package/dist/components/FloatingRootStore.js +84 -0
  28. package/dist/components/FloatingRootStore.js.map +1 -0
  29. package/dist/components/FloatingTree.cjs +33 -0
  30. package/dist/components/FloatingTree.cjs.map +1 -0
  31. package/dist/components/FloatingTree.js +22 -0
  32. package/dist/components/FloatingTree.js.map +1 -0
  33. package/dist/composite/composite.cjs +6 -0
  34. package/dist/composite/composite.js +1 -1
  35. package/dist/content/ScrollAreaContent.cjs +3 -3
  36. package/dist/content/ScrollAreaContent.cjs.map +1 -1
  37. package/dist/content/ScrollAreaContent.js +1 -1
  38. package/dist/control/FieldControl.cjs +3 -3
  39. package/dist/control/FieldControl.cjs.map +1 -1
  40. package/dist/control/FieldControl.js +1 -1
  41. package/dist/control/SliderControl.cjs +3 -3
  42. package/dist/control/SliderControl.cjs.map +1 -1
  43. package/dist/control/SliderControl.js +3 -3
  44. package/dist/corner/ScrollAreaCorner.cjs +3 -3
  45. package/dist/corner/ScrollAreaCorner.cjs.map +1 -1
  46. package/dist/corner/ScrollAreaCorner.js +1 -1
  47. package/dist/decrement/NumberFieldDecrement.cjs +868 -0
  48. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  49. package/dist/decrement/NumberFieldDecrement.js +701 -0
  50. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  51. package/dist/description/FieldDescription.cjs +2 -2
  52. package/dist/description/FieldDescription.cjs.map +1 -1
  53. package/dist/description/FieldDescription.js +1 -1
  54. package/dist/error/FieldError.cjs +2 -2
  55. package/dist/error/FieldError.cjs.map +1 -1
  56. package/dist/error/FieldError.js +1 -1
  57. package/dist/fallback/AvatarFallback.cjs +4 -48
  58. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  59. package/dist/fallback/AvatarFallback.js +4 -42
  60. package/dist/fallback/AvatarFallback.js.map +1 -1
  61. package/dist/floating-ui-vue/index.js +638 -0
  62. package/dist/floating-ui-vue/index.js.map +1 -0
  63. package/dist/form/Form.cjs +3 -3
  64. package/dist/form/Form.cjs.map +1 -1
  65. package/dist/form/Form.js +1 -1
  66. package/dist/group/NumberFieldGroup.cjs +72 -0
  67. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  68. package/dist/group/NumberFieldGroup.js +67 -0
  69. package/dist/group/NumberFieldGroup.js.map +1 -0
  70. package/dist/group/ToolbarGroup.cjs +2 -2
  71. package/dist/group/ToolbarGroup.cjs.map +1 -1
  72. package/dist/group/ToolbarGroup.js +1 -1
  73. package/dist/header/AccordionHeader.cjs +2 -2
  74. package/dist/header/AccordionHeader.cjs.map +1 -1
  75. package/dist/header/AccordionHeader.js +1 -1
  76. package/dist/hooks/useDismiss.cjs +502 -0
  77. package/dist/hooks/useDismiss.cjs.map +1 -0
  78. package/dist/hooks/useHoverFloatingInteraction.cjs +233 -0
  79. package/dist/hooks/useHoverFloatingInteraction.cjs.map +1 -0
  80. package/dist/image/AvatarImage.cjs +4 -4
  81. package/dist/image/AvatarImage.cjs.map +1 -1
  82. package/dist/image/AvatarImage.js +1 -1
  83. package/dist/increment/NumberFieldIncrement.cjs +113 -0
  84. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  85. package/dist/increment/NumberFieldIncrement.js +108 -0
  86. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  87. package/dist/index.cjs +80 -8
  88. package/dist/index.d.cts +4328 -1600
  89. package/dist/index.d.cts.map +1 -1
  90. package/dist/index.d.ts +4328 -1600
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +8 -4
  93. package/dist/index2.cjs +9435 -3236
  94. package/dist/index2.cjs.map +1 -1
  95. package/dist/index2.js +8905 -3049
  96. package/dist/index2.js.map +1 -1
  97. package/dist/rolldown-runtime.js +36 -0
  98. package/package.json +1 -1
  99. package/src/floating-ui-vue/components/FloatingPortal.ts +171 -0
  100. package/src/floating-ui-vue/components/FloatingPortal.vue +259 -0
  101. package/src/floating-ui-vue/components/FloatingRootStore.ts +83 -0
  102. package/src/floating-ui-vue/components/FloatingTree.ts +64 -0
  103. package/src/floating-ui-vue/components/FloatingTreeStore.ts +21 -0
  104. package/src/floating-ui-vue/hooks/useDismiss.ts +689 -0
  105. package/src/floating-ui-vue/hooks/useHoverFloatingInteraction.ts +244 -0
  106. package/src/floating-ui-vue/hooks/useHoverInteractionSharedState.ts +125 -0
  107. package/src/floating-ui-vue/hooks/useHoverShared.ts +53 -0
  108. package/src/floating-ui-vue/index.ts +48 -0
  109. package/src/floating-ui-vue/middleware/arrow.ts +114 -0
  110. package/src/floating-ui-vue/types.ts +117 -0
  111. package/src/floating-ui-vue/utils/composite.ts +20 -0
  112. package/src/floating-ui-vue/utils/constants.ts +2 -0
  113. package/src/floating-ui-vue/utils/createAttribute.ts +3 -0
  114. package/src/floating-ui-vue/utils/createEventEmitter.ts +20 -0
  115. package/src/floating-ui-vue/utils/element.ts +59 -0
  116. package/src/floating-ui-vue/utils/event.ts +15 -0
  117. package/src/floating-ui-vue/utils/nodes.ts +14 -0
  118. package/src/floating-ui-vue/utils/tabbable.ts +309 -0
  119. package/src/index.ts +7 -1
  120. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  121. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  122. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  123. package/src/number-field/index.ts +42 -0
  124. package/src/number-field/input/NumberFieldInput.vue +455 -0
  125. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  126. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  127. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  128. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  129. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  130. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  131. package/src/number-field/utils/constants.ts +4 -0
  132. package/src/number-field/utils/getViewportRect.ts +34 -0
  133. package/src/number-field/utils/parse.ts +248 -0
  134. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  135. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  136. package/src/number-field/utils/types.ts +24 -0
  137. package/src/number-field/utils/validate.ts +120 -0
  138. package/src/otp-field/index.ts +22 -0
  139. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  140. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  141. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  142. package/src/otp-field/utils/otp.ts +135 -0
  143. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  144. package/src/radio/index.ts +28 -0
  145. package/src/radio/indicator/RadioIndicator.vue +90 -0
  146. package/src/radio/indicator/RadioIndicatorDataAttributes.ts +54 -0
  147. package/src/radio/root/RadioRoot.vue +544 -0
  148. package/src/radio/root/RadioRootContext.ts +18 -0
  149. package/src/radio/root/RadioRootDataAttributes.ts +46 -0
  150. package/src/radio/utils/stateAttributesMapping.ts +29 -0
  151. package/src/radio-group/RadioGroup.vue +381 -0
  152. package/src/radio-group/RadioGroupContext.ts +42 -0
  153. package/src/radio-group/RadioGroupDataAttributes.ts +6 -0
  154. package/src/radio-group/index.ts +10 -0
  155. package/src/tabs/index.ts +38 -0
  156. package/src/tabs/indicator/TabsIndicator.vue +201 -0
  157. package/src/tabs/indicator/TabsIndicatorCssVars.ts +26 -0
  158. package/src/tabs/indicator/TabsIndicatorDataAttributes.ts +12 -0
  159. package/src/tabs/indicator/prehydrationScript.min.ts +5 -0
  160. package/src/tabs/indicator/prehydrationScript.template.js +87 -0
  161. package/src/tabs/list/TabsList.vue +201 -0
  162. package/src/tabs/list/TabsListContext.ts +29 -0
  163. package/src/tabs/list/TabsListDataAttributes.ts +12 -0
  164. package/src/tabs/panel/TabsPanel.vue +167 -0
  165. package/src/tabs/panel/TabsPanelDataAttributes.ts +28 -0
  166. package/src/tabs/root/TabsRoot.vue +468 -0
  167. package/src/tabs/root/TabsRootContext.ts +56 -0
  168. package/src/tabs/root/TabsRootDataAttributes.ts +12 -0
  169. package/src/tabs/root/stateAttributesMapping.ts +9 -0
  170. package/src/tabs/tab/TabsTab.vue +274 -0
  171. package/src/tabs/tab/TabsTabDataAttributes.ts +20 -0
  172. package/src/tabs/utils/areTabValuesEqual.ts +6 -0
  173. package/src/tooltip/arrow/TooltipArrow.vue +116 -0
  174. package/src/tooltip/arrow/TooltipArrowDataAttributes.ts +31 -0
  175. package/src/tooltip/index.ts +54 -0
  176. package/src/tooltip/popup/TooltipPopup.vue +142 -0
  177. package/src/tooltip/popup/TooltipPopupDataAttributes.ts +35 -0
  178. package/src/tooltip/portal/TooltipPortal.vue +83 -0
  179. package/src/tooltip/portal/TooltipPortalContext.ts +19 -0
  180. package/src/tooltip/positioner/TooltipPositioner.vue +221 -0
  181. package/src/tooltip/positioner/TooltipPositionerContext.ts +23 -0
  182. package/src/tooltip/positioner/TooltipPositionerCssVars.ts +27 -0
  183. package/src/tooltip/positioner/TooltipPositionerDataAttributes.ts +26 -0
  184. package/src/tooltip/provider/TooltipProvider.vue +78 -0
  185. package/src/tooltip/provider/TooltipProviderContext.ts +19 -0
  186. package/src/tooltip/root/TooltipRoot.vue +401 -0
  187. package/src/tooltip/root/TooltipRootContext.ts +71 -0
  188. package/src/tooltip/store/TooltipHandle.ts +215 -0
  189. package/src/tooltip/trigger/TooltipTrigger.vue +343 -0
  190. package/src/tooltip/trigger/TooltipTriggerDataAttributes.ts +12 -0
  191. package/src/tooltip/utils/constants.ts +2 -0
  192. package/src/tooltip/viewport/TooltipViewport.vue +149 -0
  193. package/src/tooltip/viewport/TooltipViewportCssVars.ts +14 -0
  194. package/src/tooltip/viewport/TooltipViewportDataAttributes.ts +26 -0
  195. package/src/utils/FloatingPortalLite.vue +66 -0
  196. package/src/utils/FocusGuard.vue +36 -0
  197. package/src/utils/adaptiveOriginMiddleware.ts +79 -0
  198. package/src/utils/addEventListener.ts +16 -0
  199. package/src/utils/constants.ts +11 -0
  200. package/src/utils/createBaseUIEventDetails.ts +2 -0
  201. package/src/utils/detectBrowser.ts +15 -0
  202. package/src/utils/formatNumber.ts +35 -2
  203. package/src/utils/getCssDimensions.ts +19 -0
  204. package/src/utils/getDisabledMountTransitionStyles.ts +9 -0
  205. package/src/utils/hideMiddleware.ts +19 -0
  206. package/src/utils/mergeCleanups.ts +7 -0
  207. package/src/utils/platform.ts +18 -0
  208. package/src/utils/popupStateMapping.ts +103 -0
  209. package/src/utils/reasons.ts +4 -0
  210. package/src/utils/serializeValue.ts +17 -0
  211. package/src/utils/useAnchorPositioning.ts +644 -0
  212. package/src/utils/useInterval.ts +45 -0
  213. package/src/utils/usePopupAutoResize.ts +254 -0
  214. package/src/utils/usePopupViewport.ts +398 -0
  215. package/src/utils/usePositioner.ts +61 -0
  216. package/src/utils/usePressAndHold.ts +260 -0
  217. package/src/utils/useRenderElement.ts +3 -0
  218. package/src/utils/useValueChanged.ts +21 -0
@@ -0,0 +1,381 @@
1
+ <script setup lang="ts" generic="Value = unknown">
2
+ import type { InjectionKey, Ref } from 'vue'
3
+ import type { FieldRootState } from '../field/root/FieldRoot.vue'
4
+ import type { BaseUIChangeEventDetails } from '../utils/createBaseUIEventDetails'
5
+ import type { REASONS } from '../utils/reasons'
6
+ import type { BaseUIComponentProps } from '../utils/types'
7
+ import type { RadioGroupContext } from './RadioGroupContext'
8
+ import { computed, provide, ref, useAttrs, watch } from 'vue'
9
+ import { SHIFT } from '../composite/composite'
10
+ import CompositeRoot from '../composite/root/CompositeRoot.vue'
11
+ import { useFieldRootContext } from '../field/root/FieldRootContext'
12
+ import { useField } from '../field/useField'
13
+ import { fieldValidityMapping } from '../field/utils/constants'
14
+ import { useFieldsetRootContext } from '../fieldset/root/FieldsetRootContext'
15
+ import { useFormContext } from '../form/FormContext'
16
+ import { useLabelableContext } from '../labelable-provider/LabelableContext'
17
+ import { mergeProps } from '../merge-props/mergeProps'
18
+ import { Slot } from '../utils/slot'
19
+ import { useBaseUiId } from '../utils/useBaseUiId'
20
+ import { useControllableState } from '../utils/useControllableState'
21
+ import { useMergedRefs } from '../utils/useMergedRefs'
22
+ import { radioGroupContextKey } from './RadioGroupContext'
23
+
24
+ export interface RadioGroupState extends FieldRootState {
25
+ /**
26
+ * Whether the component should ignore user interaction.
27
+ */
28
+ disabled: boolean
29
+ /**
30
+ * Whether the user should be unable to select a different radio button.
31
+ */
32
+ readOnly: boolean
33
+ /**
34
+ * Whether the user must choose a radio button before submitting a form.
35
+ */
36
+ required: boolean
37
+ }
38
+
39
+ export interface RadioGroupProps<Value = unknown> extends BaseUIComponentProps<RadioGroupState> {
40
+ /**
41
+ * Whether the component should ignore user interaction.
42
+ * @default false
43
+ */
44
+ disabled?: boolean
45
+ /**
46
+ * Whether the user should be unable to select a different radio button.
47
+ * @default false
48
+ */
49
+ readOnly?: boolean
50
+ /**
51
+ * Whether the user must choose a value before submitting a form.
52
+ * @default false
53
+ */
54
+ required?: boolean
55
+ /**
56
+ * Identifies the field when a form is submitted.
57
+ */
58
+ name?: string
59
+ /**
60
+ * Identifies the form that owns the radio inputs.
61
+ * Useful when the radio group is rendered outside the form.
62
+ */
63
+ form?: string
64
+ /**
65
+ * The controlled value of the radio item that should be selected.
66
+ *
67
+ * To render an uncontrolled radio group, use the `defaultValue` prop instead.
68
+ */
69
+ value?: Value
70
+ /**
71
+ * The uncontrolled value of the radio button that should be initially selected.
72
+ *
73
+ * To render a controlled radio group, use the `value` prop instead.
74
+ */
75
+ defaultValue?: Value
76
+ /**
77
+ * A ref to access the currently selected hidden input element.
78
+ */
79
+ inputRef?: Ref<HTMLInputElement | null> | ((element: HTMLInputElement | null) => void) | null
80
+ /**
81
+ * The id of the radio group element.
82
+ */
83
+ id?: string
84
+ }
85
+
86
+ export type RadioGroupChangeEventReason = typeof REASONS.none
87
+ export type RadioGroupChangeEventDetails = BaseUIChangeEventDetails<RadioGroupChangeEventReason>
88
+
89
+ /**
90
+ * Provides shared state to a series of radio buttons.
91
+ * Renders a `<div>` element.
92
+ *
93
+ * Documentation: [Base UI Vue Radio](https://baseui-vue.com/docs/components/radio)
94
+ */
95
+ defineOptions({
96
+ name: 'RadioGroup',
97
+ inheritAttrs: false,
98
+ })
99
+
100
+ const props = withDefaults(defineProps<RadioGroupProps<Value>>(), {
101
+ as: 'div',
102
+ disabled: false,
103
+ readOnly: false,
104
+ required: false,
105
+ })
106
+
107
+ const emit = defineEmits<{
108
+ /**
109
+ * Event handler called when the group value changes.
110
+ */
111
+ valueChange: [
112
+ value: Value,
113
+ eventDetails: BaseUIChangeEventDetails<typeof REASONS.none>,
114
+ ]
115
+ }>()
116
+
117
+ const MODIFIER_KEYS = [SHIFT]
118
+
119
+ const attrs = useAttrs()
120
+ const attrsObject = attrs as Record<string, unknown>
121
+
122
+ const {
123
+ disabled: fieldDisabled,
124
+ name: fieldName,
125
+ state: fieldState,
126
+ validation,
127
+ setDirty,
128
+ setFilled,
129
+ setFocused,
130
+ setTouched: setFieldTouched,
131
+ validationMode,
132
+ shouldValidateOnChange,
133
+ validityData,
134
+ } = useFieldRootContext()
135
+ const fieldsetContext = useFieldsetRootContext(true)
136
+ const labelableContext = useLabelableContext()
137
+ const { clearErrors } = useFormContext()
138
+
139
+ const disabled = computed(() =>
140
+ fieldDisabled.value
141
+ || Boolean(fieldsetContext?.disabled.value)
142
+ || props.disabled,
143
+ )
144
+ const name = computed(() => fieldName.value ?? props.name)
145
+ const groupId = useBaseUiId(props.id)
146
+ const id = computed(() => props.id ?? groupId)
147
+
148
+ const { value: checkedValue, setValue: setCheckedValueState } = useControllableState<unknown>({
149
+ controlled: () => props.value,
150
+ default: () => props.defaultValue,
151
+ name: 'RadioGroup',
152
+ state: 'value',
153
+ })
154
+
155
+ const touched = ref(false)
156
+ const controlRef = ref<HTMLElement | null>(null)
157
+ const groupInputRef = ref<HTMLInputElement | null>(null)
158
+ let currentInputElement: HTMLInputElement | null = null
159
+ let firstEnabledInputElement: HTMLInputElement | null = null
160
+ const setInputRef = useMergedRefs(
161
+ groupInputRef,
162
+ props.inputRef,
163
+ (element: HTMLInputElement | null) => {
164
+ validation.setInputRef(element)
165
+ },
166
+ )
167
+
168
+ function assignInputRef(element: HTMLInputElement | null) {
169
+ if (currentInputElement === element) {
170
+ return
171
+ }
172
+
173
+ currentInputElement = element
174
+ setInputRef?.(element)
175
+ }
176
+
177
+ function setCheckedValue(
178
+ value: Value,
179
+ eventDetails: BaseUIChangeEventDetails<typeof REASONS.none>,
180
+ ) {
181
+ emit('valueChange', value, eventDetails)
182
+
183
+ if (eventDetails.isCanceled) {
184
+ return
185
+ }
186
+
187
+ setCheckedValueState(value)
188
+ }
189
+
190
+ function registerControlRef(element: HTMLElement | null, isDisabled = false) {
191
+ if (!element) {
192
+ return
193
+ }
194
+
195
+ if (isDisabled) {
196
+ if (controlRef.value === element) {
197
+ controlRef.value = null
198
+ }
199
+ return
200
+ }
201
+
202
+ if (!controlRef.value) {
203
+ controlRef.value = element
204
+ }
205
+ }
206
+
207
+ function registerInputRef(input: HTMLInputElement | null, checked = Boolean(input?.checked)) {
208
+ if (!input || input.disabled) {
209
+ return
210
+ }
211
+
212
+ if (!firstEnabledInputElement) {
213
+ firstEnabledInputElement = input
214
+ }
215
+
216
+ if (checked || currentInputElement == null || currentInputElement.disabled) {
217
+ assignInputRef(input)
218
+ }
219
+ }
220
+
221
+ function getFormValue() {
222
+ const input = currentInputElement
223
+ if (!input || input.disabled || !input.checked) {
224
+ return null
225
+ }
226
+
227
+ return checkedValue.value ?? null
228
+ }
229
+
230
+ function combineDescriptionProps(
231
+ ...sources: object[]
232
+ ) {
233
+ const describedBy = Array.from(
234
+ new Set(
235
+ sources
236
+ .map(source => (source as { 'aria-describedby'?: unknown })['aria-describedby'])
237
+ .filter(Boolean)
238
+ .flatMap(value => String(value).split(/\s+/).filter(Boolean)),
239
+ ),
240
+ ).join(' ') || undefined
241
+
242
+ return {
243
+ ...Object.assign({}, ...sources),
244
+ 'aria-describedby': describedBy,
245
+ }
246
+ }
247
+
248
+ useField({
249
+ enabled: computed(() => Boolean(name.value)),
250
+ id,
251
+ commit: (value: unknown) => validation.commit(value),
252
+ value: checkedValue as Ref<unknown>,
253
+ controlRef,
254
+ name,
255
+ getValue: getFormValue,
256
+ })
257
+
258
+ watch(
259
+ () => checkedValue.value,
260
+ (nextValue) => {
261
+ clearErrors(name.value)
262
+ setDirty(nextValue !== validityData.value.initialValue)
263
+ setFilled(nextValue != null)
264
+
265
+ if (shouldValidateOnChange()) {
266
+ void validation.commit(nextValue)
267
+ }
268
+ else {
269
+ void validation.commit(nextValue, true)
270
+ }
271
+
272
+ const fallbackInput = firstEnabledInputElement
273
+ if (nextValue == null && fallbackInput && !fallbackInput.disabled) {
274
+ assignInputRef(fallbackInput)
275
+ }
276
+ },
277
+ )
278
+
279
+ function handleFocusIn() {
280
+ setFocused(true)
281
+ }
282
+
283
+ function handleFocusOut(event: FocusEvent) {
284
+ const currentTarget = event.currentTarget as HTMLElement
285
+ const relatedTarget = event.relatedTarget as Node | null
286
+ if (relatedTarget && currentTarget.contains(relatedTarget)) {
287
+ return
288
+ }
289
+
290
+ setFieldTouched(true)
291
+ setFocused(false)
292
+
293
+ if (validationMode.value === 'onBlur') {
294
+ void validation.commit(checkedValue.value)
295
+ }
296
+ }
297
+
298
+ function handleKeydownCapture(event: KeyboardEvent) {
299
+ if (event.key.startsWith('Arrow')) {
300
+ touched.value = true
301
+ setFocused(true)
302
+ }
303
+ }
304
+
305
+ const state = computed<RadioGroupState>(() => ({
306
+ ...fieldState.value,
307
+ disabled: disabled.value,
308
+ readOnly: props.readOnly,
309
+ required: props.required,
310
+ }))
311
+
312
+ const contextValue: RadioGroupContext<Value> = {
313
+ disabled,
314
+ readOnly: computed(() => props.readOnly),
315
+ required: computed(() => props.required),
316
+ form: computed(() => props.form),
317
+ name,
318
+ checkedValue: checkedValue as Readonly<Ref<Value | undefined>>,
319
+ touched,
320
+ validation,
321
+ setCheckedValue,
322
+ setTouched(value) {
323
+ touched.value = value
324
+ },
325
+ registerControlRef,
326
+ registerInputRef,
327
+ }
328
+
329
+ provide(radioGroupContextKey as InjectionKey<RadioGroupContext<Value>>, contextValue)
330
+
331
+ const forwardedAttrs = computed(() => {
332
+ const {
333
+ class: _class,
334
+ style: _style,
335
+ 'aria-describedby': _ariaDescribedBy,
336
+ ...rest
337
+ } = attrsObject
338
+ return rest
339
+ })
340
+
341
+ const rootProps = computed(() => mergeProps(
342
+ combineDescriptionProps(
343
+ { 'aria-describedby': attrsObject['aria-describedby'] },
344
+ labelableContext.getDescriptionProps(),
345
+ validation.getValidationProps(),
346
+ ),
347
+ {
348
+ 'id': id.value,
349
+ 'role': 'radiogroup',
350
+ 'aria-required': props.required || undefined,
351
+ 'aria-disabled': disabled.value || undefined,
352
+ 'aria-readonly': props.readOnly || undefined,
353
+ 'aria-labelledby': attrsObject['aria-labelledby'] ?? labelableContext.labelId.value ?? fieldsetContext?.legendId.value,
354
+ 'onFocusin': handleFocusIn,
355
+ 'onFocusout': handleFocusOut,
356
+ 'onKeydownCapture': handleKeydownCapture,
357
+ },
358
+ forwardedAttrs.value,
359
+ ))
360
+ </script>
361
+
362
+ <template>
363
+ <CompositeRoot
364
+ :as="as"
365
+ :class="props.class"
366
+ :style="props.style"
367
+ :state="state"
368
+ :state-attributes-mapping="fieldValidityMapping"
369
+ :enable-home-and-end-keys="false"
370
+ :modifier-keys="MODIFIER_KEYS"
371
+ role="radiogroup"
372
+ v-bind="rootProps"
373
+ >
374
+ <template v-if="as === Slot" #default="{ ref: rootRef, props: compositeProps, state: compositeState }">
375
+ <slot :ref="rootRef" :props="compositeProps" :state="compositeState" />
376
+ </template>
377
+ <template v-else #default="{ state: compositeState }">
378
+ <slot :state="compositeState" />
379
+ </template>
380
+ </CompositeRoot>
381
+ </template>
@@ -0,0 +1,42 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import type { UseFieldValidationReturnValue } from '../field/root/useFieldValidation'
3
+ import type { BaseUIChangeEventDetails } from '../utils/createBaseUIEventDetails'
4
+ import type { REASONS } from '../utils/reasons'
5
+ import { inject } from 'vue'
6
+
7
+ export interface RadioGroupContext<Value = unknown> {
8
+ disabled: Readonly<Ref<boolean | undefined>>
9
+ readOnly: Readonly<Ref<boolean | undefined>>
10
+ required: Readonly<Ref<boolean | undefined>>
11
+ form: Readonly<Ref<string | undefined>>
12
+ name: Readonly<Ref<string | undefined>>
13
+ checkedValue: Readonly<Ref<Value | undefined>>
14
+ touched: Readonly<Ref<boolean>>
15
+ validation?: UseFieldValidationReturnValue
16
+ setCheckedValue: (
17
+ value: Value,
18
+ eventDetails: BaseUIChangeEventDetails<typeof REASONS.none>,
19
+ ) => void
20
+ setTouched: (value: boolean) => void
21
+ registerControlRef: (element: HTMLElement | null, disabled?: boolean) => void
22
+ registerInputRef: (element: HTMLInputElement | null, checked?: boolean) => void
23
+ }
24
+
25
+ export const radioGroupContextKey: InjectionKey<RadioGroupContext<unknown>> = Symbol(
26
+ 'RadioGroupContext',
27
+ )
28
+
29
+ export function useRadioGroupContext<Value = unknown>(
30
+ optional: true,
31
+ ): RadioGroupContext<Value> | undefined
32
+ export function useRadioGroupContext<Value = unknown>(optional?: false): RadioGroupContext<Value>
33
+ export function useRadioGroupContext<Value = unknown>(optional = false) {
34
+ const context = inject(radioGroupContextKey, undefined) as RadioGroupContext<Value> | undefined
35
+ if (!context && !optional) {
36
+ throw new Error(
37
+ 'Base UI Vue: RadioGroupContext is missing. Radio parts must be placed within a radio group.',
38
+ )
39
+ }
40
+
41
+ return context
42
+ }
@@ -0,0 +1,6 @@
1
+ export enum RadioGroupDataAttributes {
2
+ /**
3
+ * Present when the radio group is disabled.
4
+ */
5
+ disabled = 'data-disabled',
6
+ }
@@ -0,0 +1,10 @@
1
+ export { default as RadioGroup } from './RadioGroup.vue'
2
+ export type {
3
+ RadioGroupChangeEventDetails,
4
+ RadioGroupChangeEventReason,
5
+ RadioGroupProps,
6
+ RadioGroupState,
7
+ } from './RadioGroup.vue'
8
+ export { radioGroupContextKey, useRadioGroupContext } from './RadioGroupContext'
9
+ export type { RadioGroupContext } from './RadioGroupContext'
10
+ export { RadioGroupDataAttributes } from './RadioGroupDataAttributes'
@@ -0,0 +1,38 @@
1
+ export { default as TabsIndicator } from './indicator/TabsIndicator.vue'
2
+ export type { TabsIndicatorProps, TabsIndicatorState } from './indicator/TabsIndicator.vue'
3
+ export { TabsIndicatorCssVars } from './indicator/TabsIndicatorCssVars'
4
+ export { TabsIndicatorDataAttributes } from './indicator/TabsIndicatorDataAttributes'
5
+
6
+ export { default as TabsList } from './list/TabsList.vue'
7
+ export type { TabsListProps, TabsListState } from './list/TabsList.vue'
8
+ export { tabsListContextKey, useTabsListContext } from './list/TabsListContext'
9
+ export type { TabsListContext } from './list/TabsListContext'
10
+ export { TabsListDataAttributes } from './list/TabsListDataAttributes'
11
+
12
+ export { default as TabsPanel } from './panel/TabsPanel.vue'
13
+ export type { TabsPanelMetadata, TabsPanelProps, TabsPanelState } from './panel/TabsPanel.vue'
14
+ export { TabsPanelDataAttributes } from './panel/TabsPanelDataAttributes'
15
+
16
+ export { default as TabsRoot } from './root/TabsRoot.vue'
17
+ export type {
18
+ TabsRootChangeEventDetails,
19
+ TabsRootChangeEventReason,
20
+ TabsRootOrientation,
21
+ TabsRootProps,
22
+ TabsRootState,
23
+ } from './root/TabsRoot.vue'
24
+ export { tabsRootContextKey, useTabsRootContext } from './root/TabsRootContext'
25
+ export type { TabsRootContext } from './root/TabsRootContext'
26
+ export { TabsRootDataAttributes } from './root/TabsRootDataAttributes'
27
+
28
+ export { default as TabsTab } from './tab/TabsTab.vue'
29
+ export type {
30
+ TabsTabActivationDirection,
31
+ TabsTabMetadata,
32
+ TabsTabPosition,
33
+ TabsTabProps,
34
+ TabsTabSize,
35
+ TabsTabState,
36
+ TabsTabValue,
37
+ } from './tab/TabsTab.vue'
38
+ export { TabsTabDataAttributes } from './tab/TabsTabDataAttributes'
@@ -0,0 +1,201 @@
1
+ <script setup lang="ts">
2
+ import type { StyleValue } from 'vue'
3
+ import type { BaseUIComponentProps, Orientation } from '../../utils/types'
4
+ import type { TabsRootState } from '../root/TabsRoot.vue'
5
+ import type { TabsTabActivationDirection, TabsTabPosition, TabsTabSize } from '../tab/TabsTab.vue'
6
+ import { computed, onBeforeUnmount, onMounted, ref, useAttrs } from 'vue'
7
+ import { useCSPContext } from '../../csp-provider/CSPContext'
8
+ import { mergeProps } from '../../merge-props/mergeProps'
9
+ import { getCssDimensions } from '../../utils/getCssDimensions'
10
+ import { useRenderElement } from '../../utils/useRenderElement'
11
+ import { useTabsListContext } from '../list/TabsListContext'
12
+ import { tabsStateAttributesMapping } from '../root/stateAttributesMapping'
13
+ import { useTabsRootContext } from '../root/TabsRootContext'
14
+ import { script as prehydrationScript } from './prehydrationScript.min'
15
+ import { TabsIndicatorCssVars } from './TabsIndicatorCssVars'
16
+
17
+ export interface TabsIndicatorState extends TabsRootState {
18
+ /**
19
+ * The active tab position.
20
+ */
21
+ activeTabPosition: TabsTabPosition | null
22
+ /**
23
+ * The active tab size.
24
+ */
25
+ activeTabSize: TabsTabSize | null
26
+ /**
27
+ * The component orientation.
28
+ */
29
+ orientation: Orientation
30
+ /**
31
+ * The direction used for tab activation.
32
+ */
33
+ tabActivationDirection: TabsTabActivationDirection
34
+ }
35
+
36
+ export interface TabsIndicatorProps extends BaseUIComponentProps<TabsIndicatorState> {
37
+ /**
38
+ * Whether to render itself before Vue hydrates.
39
+ * This minimizes the time that the indicator isn't visible after server-side rendering.
40
+ * @default false
41
+ */
42
+ renderBeforeHydration?: boolean
43
+ }
44
+
45
+ /**
46
+ * A visual indicator that can be styled to match the position of the currently active tab.
47
+ * Renders a `<span>` element.
48
+ *
49
+ * Documentation: [Base UI Vue Tabs](https://baseui-vue.com/docs/components/tabs)
50
+ */
51
+ defineOptions({
52
+ name: 'TabsIndicator',
53
+ inheritAttrs: false,
54
+ })
55
+
56
+ const props = withDefaults(defineProps<TabsIndicatorProps>(), {
57
+ as: 'span',
58
+ renderBeforeHydration: false,
59
+ })
60
+
61
+ const attrs = useAttrs()
62
+ const { nonce } = useCSPContext()
63
+ const rootCtx = useTabsRootContext()
64
+ const listCtx = useTabsListContext()
65
+ const layoutTick = ref(0)
66
+ const isMounted = ref(false)
67
+
68
+ const cleanupIndicatorListener = listCtx.registerIndicatorUpdateListener(() => {
69
+ layoutTick.value += 1
70
+ })
71
+
72
+ onMounted(() => {
73
+ isMounted.value = true
74
+ })
75
+
76
+ onBeforeUnmount(() => {
77
+ cleanupIndicatorListener()
78
+ })
79
+
80
+ const measurement = computed(() => {
81
+ void layoutTick.value
82
+
83
+ const tabsListElement = listCtx.tabsListElement.value
84
+ const selectedValue = rootCtx.value.value
85
+
86
+ if (selectedValue == null || tabsListElement == null) {
87
+ return null
88
+ }
89
+
90
+ const activeTab = rootCtx.getTabElementBySelectedValue(selectedValue)
91
+
92
+ if (activeTab == null) {
93
+ return null
94
+ }
95
+
96
+ const { width, height } = getCssDimensions(activeTab)
97
+ const { width: tabListWidth, height: tabListHeight } = getCssDimensions(tabsListElement)
98
+ const tabRect = activeTab.getBoundingClientRect()
99
+ const tabsListRect = tabsListElement.getBoundingClientRect()
100
+ const scaleX = tabListWidth > 0 ? tabsListRect.width / tabListWidth : 1
101
+ const scaleY = tabListHeight > 0 ? tabsListRect.height / tabListHeight : 1
102
+ const hasNonZeroScale
103
+ = Math.abs(scaleX) > Number.EPSILON && Math.abs(scaleY) > Number.EPSILON
104
+
105
+ let left = 0
106
+ let top = 0
107
+
108
+ if (hasNonZeroScale) {
109
+ left = (tabRect.left - tabsListRect.left) / scaleX + tabsListElement.scrollLeft - tabsListElement.clientLeft
110
+ top = (tabRect.top - tabsListRect.top) / scaleY + tabsListElement.scrollTop - tabsListElement.clientTop
111
+ }
112
+ else {
113
+ left = activeTab.offsetLeft
114
+ top = activeTab.offsetTop
115
+ }
116
+
117
+ const right = tabsListElement.scrollWidth - left - width
118
+ const bottom = tabsListElement.scrollHeight - top - height
119
+
120
+ return {
121
+ position: { left, right, top, bottom },
122
+ size: { width, height },
123
+ }
124
+ })
125
+
126
+ const activeTabPosition = computed(() => measurement.value?.position ?? null)
127
+ const activeTabSize = computed(() => measurement.value?.size ?? null)
128
+ const displayIndicator = computed(() =>
129
+ activeTabSize.value != null && activeTabSize.value.width > 0 && activeTabSize.value.height > 0,
130
+ )
131
+
132
+ const state = computed<TabsIndicatorState>(() => ({
133
+ orientation: rootCtx.orientation.value,
134
+ activeTabPosition: activeTabPosition.value,
135
+ activeTabSize: activeTabSize.value,
136
+ tabActivationDirection: rootCtx.tabActivationDirection.value,
137
+ }))
138
+
139
+ const indicatorStyle = computed<StyleValue | undefined>(() => {
140
+ const position = activeTabPosition.value
141
+ const size = activeTabSize.value
142
+
143
+ if (position == null || size == null) {
144
+ return undefined
145
+ }
146
+
147
+ return {
148
+ [TabsIndicatorCssVars.activeTabLeft]: `${position.left}px`,
149
+ [TabsIndicatorCssVars.activeTabRight]: `${position.right}px`,
150
+ [TabsIndicatorCssVars.activeTabTop]: `${position.top}px`,
151
+ [TabsIndicatorCssVars.activeTabBottom]: `${position.bottom}px`,
152
+ [TabsIndicatorCssVars.activeTabWidth]: `${size.width}px`,
153
+ [TabsIndicatorCssVars.activeTabHeight]: `${size.height}px`,
154
+ } as StyleValue
155
+ })
156
+
157
+ const indicatorProps = computed(() => mergeProps(
158
+ {
159
+ role: 'presentation',
160
+ style: indicatorStyle.value,
161
+ hidden: !displayIndicator.value,
162
+ suppressHydrationWarning: props.renderBeforeHydration || undefined,
163
+ },
164
+ attrs as Record<string, unknown>,
165
+ ))
166
+
167
+ const stateAttributesMapping = {
168
+ ...tabsStateAttributesMapping,
169
+ activeTabPosition: () => null,
170
+ activeTabSize: () => null,
171
+ }
172
+
173
+ const {
174
+ tag,
175
+ mergedProps,
176
+ renderless,
177
+ ref: renderRef,
178
+ } = useRenderElement({
179
+ componentProps: props,
180
+ state,
181
+ props: indicatorProps,
182
+ stateAttributesMapping,
183
+ defaultTagName: 'span',
184
+ })
185
+ </script>
186
+
187
+ <template>
188
+ <template v-if="rootCtx.value.value !== null">
189
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
190
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps" />
191
+ <!-- eslint-disable-next-line vue/require-component-is -->
192
+ <component
193
+ is="script"
194
+ v-if="props.renderBeforeHydration && !isMounted"
195
+ :nonce="nonce"
196
+ :suppress-hydration-warning="true"
197
+ >
198
+ {{ prehydrationScript }}
199
+ </component>
200
+ </template>
201
+ </template>