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,544 @@
1
+ <script setup lang="ts" generic="Value = unknown">
2
+ import type { Ref } from 'vue'
3
+ import type { FieldRootState } from '../../field/root/FieldRoot.vue'
4
+ import type { BaseUIChangeEventDetails } from '../../utils/createBaseUIEventDetails'
5
+ import type { BaseUIComponentProps, NonNativeButtonProps } from '../../utils/types'
6
+ import { computed, provide, ref, useAttrs, watch, watchEffect } from 'vue'
7
+ import { ACTIVE_COMPOSITE_ITEM } from '../../composite/constants'
8
+ import CompositeItem from '../../composite/item/CompositeItem.vue'
9
+ import { useFieldItemContext } from '../../field/item/FieldItemContext'
10
+ import { useFieldRootContext } from '../../field/root/FieldRootContext'
11
+ import { useField } from '../../field/useField'
12
+ import { useFormContext } from '../../form/FormContext'
13
+ import { useLabelableContext } from '../../labelable-provider/LabelableContext'
14
+ import { useAriaLabelledBy } from '../../labelable-provider/useAriaLabelledBy'
15
+ import { useLabelableId } from '../../labelable-provider/useLabelableId'
16
+ import { mergeProps } from '../../merge-props/mergeProps'
17
+ import { useRadioGroupContext } from '../../radio-group/RadioGroupContext'
18
+ import { useButton } from '../../use-button'
19
+ import { createChangeEventDetails } from '../../utils/createBaseUIEventDetails'
20
+ import { EMPTY_OBJECT } from '../../utils/empty'
21
+ import { ownerWindow } from '../../utils/owner'
22
+ import { REASONS } from '../../utils/reasons'
23
+ import { serializeValue } from '../../utils/serializeValue'
24
+ import { useBaseUiId } from '../../utils/useBaseUiId'
25
+ import { useMergedRefs } from '../../utils/useMergedRefs'
26
+ import { useRenderElement } from '../../utils/useRenderElement'
27
+ import { visuallyHidden, visuallyHiddenInput } from '../../utils/visuallyHidden'
28
+ import { stateAttributesMapping } from '../utils/stateAttributesMapping'
29
+ import { radioRootContextKey } from './RadioRootContext'
30
+
31
+ export interface RadioRootState extends FieldRootState {
32
+ /**
33
+ * Whether the radio button is currently selected.
34
+ */
35
+ checked: boolean
36
+ /**
37
+ * Whether the component should ignore user interaction.
38
+ */
39
+ disabled: boolean
40
+ /**
41
+ * Whether the user should be unable to select the radio button.
42
+ */
43
+ readOnly: boolean
44
+ /**
45
+ * Whether the user must choose a value before submitting a form.
46
+ */
47
+ required: boolean
48
+ }
49
+
50
+ export type RadioRootChangeEventReason = typeof REASONS.none
51
+ export type RadioRootChangeEventDetails = BaseUIChangeEventDetails<RadioRootChangeEventReason>
52
+
53
+ export interface RadioRootProps<Value = unknown>
54
+ extends NonNativeButtonProps, BaseUIComponentProps<RadioRootState> {
55
+ /**
56
+ * The id of the radio control.
57
+ */
58
+ 'id'?: string
59
+ /**
60
+ * The unique identifying value of the radio in a group.
61
+ */
62
+ 'value': Value
63
+ /**
64
+ * Whether the component should ignore user interaction.
65
+ * @default false
66
+ */
67
+ 'disabled'?: boolean
68
+ /**
69
+ * Whether the user should be unable to select the radio button.
70
+ * @default false
71
+ */
72
+ 'readOnly'?: boolean
73
+ /**
74
+ * Whether the user must choose a value before submitting a form.
75
+ * @default false
76
+ */
77
+ 'required'?: boolean
78
+ /**
79
+ * A ref to access the hidden `<input>` element.
80
+ */
81
+ 'inputRef'?: Ref<HTMLInputElement | null> | ((element: HTMLInputElement | null) => void) | null
82
+ /**
83
+ * Identifies the element that labels the radio.
84
+ * When omitted, Base UI Vue falls back to the surrounding label system.
85
+ */
86
+ // eslint-disable-next-line vue/prop-name-casing
87
+ 'aria-labelledby'?: string
88
+ }
89
+
90
+ /**
91
+ * Represents the radio button itself.
92
+ * Renders a `<span>` element and a hidden `<input>` beside.
93
+ *
94
+ * Documentation: [Base UI Vue Radio](https://baseui-vue.com/docs/components/radio)
95
+ */
96
+ defineOptions({
97
+ name: 'RadioRoot',
98
+ inheritAttrs: false,
99
+ })
100
+
101
+ const props = withDefaults(defineProps<RadioRootProps<Value>>(), {
102
+ as: 'span',
103
+ disabled: false,
104
+ nativeButton: false,
105
+ readOnly: false,
106
+ required: false,
107
+ })
108
+
109
+ const attrs = useAttrs()
110
+ const attrsObject = attrs as Record<string, unknown>
111
+
112
+ const { clearErrors } = useFormContext()
113
+ const {
114
+ disabled: fieldDisabled,
115
+ name: fieldName,
116
+ setDirty,
117
+ setFilled,
118
+ setFocused,
119
+ setTouched: setFieldTouched,
120
+ state: fieldState,
121
+ validationMode,
122
+ validityData,
123
+ shouldValidateOnChange,
124
+ validation: localValidation,
125
+ } = useFieldRootContext()
126
+ const fieldItemContext = useFieldItemContext()
127
+ const labelableContext = useLabelableContext()
128
+ const groupContext = useRadioGroupContext<Value>(true)
129
+
130
+ const disabled = computed(() =>
131
+ fieldDisabled.value
132
+ || fieldItemContext.disabled.value
133
+ || Boolean(groupContext?.disabled.value)
134
+ || props.disabled,
135
+ )
136
+ const readOnly = computed(() => Boolean(groupContext?.readOnly.value) || props.readOnly)
137
+ const required = computed(() => Boolean(groupContext?.required.value) || props.required)
138
+ const name = computed(() => groupContext?.name.value ?? fieldName.value)
139
+ const form = computed(() => groupContext?.form.value)
140
+ const validation = groupContext?.validation ?? localValidation
141
+
142
+ const checked = computed(() => {
143
+ if (groupContext) {
144
+ return groupContext.checkedValue.value === props.value
145
+ }
146
+
147
+ return props.value === ''
148
+ })
149
+
150
+ const rootElementId = useBaseUiId()
151
+ const controlId = useLabelableId({ id: () => props.id })
152
+ const inputId = computed(() => (props.nativeButton ? undefined : controlId.value))
153
+
154
+ const controlRef = ref<HTMLElement | null>(null)
155
+ const inputElementRef = ref<HTMLInputElement | null>(null)
156
+ const mergedInputRef = useMergedRefs(inputElementRef, props.inputRef)
157
+ let pendingInputClickEvent: Event | null = null
158
+
159
+ useField({
160
+ enabled: computed(() => !groupContext),
161
+ id: computed(() => rootElementId),
162
+ commit: (value: unknown) => validation.commit(value),
163
+ value: checked,
164
+ controlRef,
165
+ name,
166
+ getValue: () => checked.value ? props.value : null,
167
+ })
168
+
169
+ const ariaLabelledBy = useAriaLabelledBy({
170
+ ariaLabelledBy: computed(() => props['aria-labelledby']),
171
+ labelId: labelableContext.labelId,
172
+ labelSourceRef: inputElementRef,
173
+ enableFallback: !props.nativeButton,
174
+ labelSourceId: inputId,
175
+ })
176
+
177
+ watchEffect(() => {
178
+ if (checked.value) {
179
+ setFilled(true)
180
+ }
181
+ })
182
+
183
+ watchEffect((onCleanup) => {
184
+ const input = inputElementRef.value
185
+ if (!input) {
186
+ return
187
+ }
188
+
189
+ if (groupContext) {
190
+ const isChecked = checked.value
191
+
192
+ if (disabled.value && isChecked) {
193
+ groupContext.registerInputRef(null)
194
+ return
195
+ }
196
+
197
+ groupContext.registerInputRef(input, isChecked)
198
+
199
+ onCleanup(() => {
200
+ groupContext.registerInputRef(null)
201
+ })
202
+ return
203
+ }
204
+
205
+ validation.setInputRef(input)
206
+ })
207
+
208
+ watchEffect((onCleanup) => {
209
+ const element = controlRef.value
210
+ if (!element || !groupContext) {
211
+ return
212
+ }
213
+
214
+ groupContext.registerControlRef(element, disabled.value)
215
+
216
+ onCleanup(() => {
217
+ groupContext.registerControlRef(null)
218
+ })
219
+ })
220
+
221
+ watch(
222
+ () => checked.value,
223
+ (nextChecked) => {
224
+ if (groupContext) {
225
+ return
226
+ }
227
+
228
+ clearErrors(name.value)
229
+ setFilled(nextChecked)
230
+ setDirty((nextChecked ? props.value : null) !== validityData.value.initialValue)
231
+
232
+ const nextValue = nextChecked ? props.value : null
233
+ if (shouldValidateOnChange()) {
234
+ void validation.commit(nextValue)
235
+ }
236
+ else {
237
+ void validation.commit(nextValue, true)
238
+ }
239
+ },
240
+ )
241
+
242
+ const { getButtonProps, buttonRef } = useButton({
243
+ disabled,
244
+ native: computed(() => props.nativeButton),
245
+ })
246
+
247
+ function combineDescriptionProps<
248
+ LocalProps extends object,
249
+ ValidationProps extends object,
250
+ >(
251
+ localProps: LocalProps,
252
+ validationProps: ValidationProps,
253
+ externalProps: { 'aria-describedby'?: unknown } = {},
254
+ ): LocalProps & ValidationProps & { 'aria-describedby'?: string } {
255
+ const describedBy = Array.from(
256
+ new Set(
257
+ [
258
+ externalProps['aria-describedby'],
259
+ (localProps as { 'aria-describedby'?: unknown })['aria-describedby'],
260
+ (validationProps as { 'aria-describedby'?: unknown })['aria-describedby'],
261
+ ]
262
+ .filter(Boolean)
263
+ .flatMap(value => String(value).split(/\s+/).filter(Boolean)),
264
+ ),
265
+ ).join(' ') || undefined
266
+
267
+ return {
268
+ ...localProps,
269
+ ...validationProps,
270
+ 'aria-describedby': describedBy,
271
+ }
272
+ }
273
+
274
+ function createInputClickEvent(event: MouseEvent | KeyboardEvent) {
275
+ const input = inputElementRef.value
276
+ if (!input) {
277
+ return undefined
278
+ }
279
+
280
+ const win = ownerWindow(input)
281
+ const PointerEventCtor = win.PointerEvent ?? win.MouseEvent
282
+
283
+ return new PointerEventCtor('click', {
284
+ bubbles: true,
285
+ shiftKey: event.shiftKey,
286
+ ctrlKey: event.ctrlKey,
287
+ altKey: event.altKey,
288
+ metaKey: event.metaKey,
289
+ })
290
+ }
291
+
292
+ function applyCheckedValue(event: Event) {
293
+ if (disabled.value || readOnly.value || props.value === undefined) {
294
+ return false
295
+ }
296
+
297
+ const details = createChangeEventDetails(REASONS.none, pendingInputClickEvent ?? event)
298
+
299
+ if (details.isCanceled) {
300
+ return false
301
+ }
302
+
303
+ if (groupContext) {
304
+ groupContext.setCheckedValue(props.value, details)
305
+ if (details.isCanceled) {
306
+ return false
307
+ }
308
+
309
+ setFieldTouched(true)
310
+ return !details.isCanceled
311
+ }
312
+
313
+ setFieldTouched(true)
314
+ setDirty(props.value !== validityData.value.initialValue)
315
+ setFilled(true)
316
+
317
+ clearErrors(name.value)
318
+
319
+ if (shouldValidateOnChange()) {
320
+ void validation.commit(props.value)
321
+ }
322
+ else {
323
+ void validation.commit(props.value, true)
324
+ }
325
+
326
+ return !details.isCanceled
327
+ }
328
+
329
+ function handleInputClick(event: MouseEvent) {
330
+ pendingInputClickEvent = event
331
+
332
+ queueMicrotask(() => {
333
+ if (pendingInputClickEvent === event) {
334
+ pendingInputClickEvent = null
335
+ }
336
+ })
337
+ }
338
+
339
+ function handleInputChange(event: Event) {
340
+ if (event.defaultPrevented) {
341
+ return
342
+ }
343
+
344
+ const target = event.currentTarget as HTMLInputElement
345
+ const applied = applyCheckedValue(event)
346
+ pendingInputClickEvent = null
347
+
348
+ if (!applied) {
349
+ target.checked = checked.value
350
+ }
351
+ }
352
+
353
+ function handleRootClick(event: MouseEvent | KeyboardEvent) {
354
+ if (event.defaultPrevented || disabled.value || readOnly.value) {
355
+ return
356
+ }
357
+
358
+ event.preventDefault()
359
+
360
+ const inputClickEvent = createInputClickEvent(event)
361
+ if (!inputClickEvent) {
362
+ return
363
+ }
364
+
365
+ inputElementRef.value?.dispatchEvent(inputClickEvent)
366
+ }
367
+
368
+ function handleRootKeydown(event: KeyboardEvent) {
369
+ if (event.key === 'Enter') {
370
+ event.preventDefault()
371
+ ;(event as KeyboardEvent & { preventBaseUIHandler?: () => void }).preventBaseUIHandler?.()
372
+ }
373
+ }
374
+
375
+ function handleRootFocus(event: FocusEvent) {
376
+ if (event.defaultPrevented || disabled.value || readOnly.value) {
377
+ return
378
+ }
379
+
380
+ if (groupContext?.touched.value) {
381
+ inputElementRef.value?.click()
382
+ groupContext.setTouched(false)
383
+ return
384
+ }
385
+
386
+ if (!groupContext) {
387
+ setFocused(true)
388
+ }
389
+ }
390
+
391
+ function handleRootBlur() {
392
+ if (disabled.value || groupContext) {
393
+ return
394
+ }
395
+
396
+ setFieldTouched(true)
397
+ setFocused(false)
398
+
399
+ if (validationMode.value === 'onBlur') {
400
+ void validation.commit(checked.value ? props.value : null)
401
+ }
402
+ }
403
+
404
+ const state = computed<RadioRootState>(() => ({
405
+ ...fieldState.value,
406
+ checked: checked.value,
407
+ disabled: disabled.value,
408
+ readOnly: readOnly.value,
409
+ required: required.value,
410
+ }))
411
+
412
+ provide(radioRootContextKey, state)
413
+
414
+ defineExpose({
415
+ element: controlRef,
416
+ inputElement: inputElementRef,
417
+ })
418
+
419
+ const mergedRootRef = useMergedRefs(buttonRef, controlRef)
420
+
421
+ const buttonInteractionAttrs = computed(() => ({
422
+ onClick: attrsObject.onClick,
423
+ onKeydown: attrsObject.onKeydown,
424
+ onKeyup: attrsObject.onKeyup,
425
+ onMousedown: attrsObject.onMousedown,
426
+ onPointerdown: attrsObject.onPointerdown,
427
+ }))
428
+
429
+ const passthroughAttrs = computed(() => {
430
+ const {
431
+ onClick,
432
+ onKeydown,
433
+ onKeyup,
434
+ onMousedown,
435
+ onPointerdown,
436
+ 'aria-describedby': _ariaDescribedBy,
437
+ ...rest
438
+ } = attrsObject
439
+
440
+ return rest
441
+ })
442
+
443
+ const rootProps = computed(() => {
444
+ const localDescriptionProps = labelableContext.getDescriptionProps()
445
+ const validationProps = validation.getValidationProps()
446
+ const buttonProps = getButtonProps(
447
+ mergeProps(buttonInteractionAttrs.value, {
448
+ onClick: handleRootClick,
449
+ onKeydown: handleRootKeydown,
450
+ }),
451
+ )
452
+
453
+ return mergeProps(
454
+ buttonProps,
455
+ combineDescriptionProps(
456
+ localDescriptionProps,
457
+ validationProps,
458
+ { 'aria-describedby': attrsObject['aria-describedby'] },
459
+ ),
460
+ {
461
+ 'id': props.nativeButton ? controlId.value : rootElementId,
462
+ 'role': 'radio',
463
+ 'aria-checked': checked.value,
464
+ 'aria-readonly': readOnly.value || undefined,
465
+ 'aria-required': required.value || undefined,
466
+ 'aria-labelledby': ariaLabelledBy.value,
467
+ [ACTIVE_COMPOSITE_ITEM]: checked.value ? '' : undefined,
468
+ 'onFocus': handleRootFocus,
469
+ 'onBlur': handleRootBlur,
470
+ },
471
+ passthroughAttrs.value,
472
+ )
473
+ })
474
+
475
+ const {
476
+ tag,
477
+ mergedProps,
478
+ renderless,
479
+ ref: renderRef,
480
+ } = useRenderElement({
481
+ componentProps: props,
482
+ state,
483
+ props: rootProps,
484
+ stateAttributesMapping,
485
+ defaultTagName: 'span',
486
+ ref: mergedRootRef,
487
+ })
488
+
489
+ const compositeItemRefs = mergedRootRef ? [mergedRootRef] : []
490
+ const compositeItemProps = [() => rootProps.value]
491
+
492
+ const inputProps = computed(() => {
493
+ const validationProps = groupContext
494
+ ? validation.getValidationProps()
495
+ : validation.getInputValidationProps()
496
+
497
+ return mergeProps(
498
+ {
499
+ 'checked': checked.value,
500
+ 'disabled': disabled.value,
501
+ 'form': form.value,
502
+ 'id': inputId.value,
503
+ 'name': name.value,
504
+ 'readOnly': readOnly.value,
505
+ 'required': required.value,
506
+ 'ref': mergedInputRef,
507
+ 'type': 'radio',
508
+ 'aria-hidden': true,
509
+ 'tabindex': -1,
510
+ 'style': name.value ? visuallyHiddenInput : visuallyHidden,
511
+ 'onClick': handleInputClick,
512
+ 'onChange': handleInputChange,
513
+ onFocus() {
514
+ controlRef.value?.focus()
515
+ },
516
+ },
517
+ props.value !== undefined
518
+ ? { value: serializeValue(props.value) }
519
+ : EMPTY_OBJECT,
520
+ validationProps,
521
+ )
522
+ })
523
+ </script>
524
+
525
+ <template>
526
+ <CompositeItem
527
+ v-if="groupContext"
528
+ v-slot="slotProps"
529
+ :as="as"
530
+ :class="props.class"
531
+ :style="props.style"
532
+ :state="state"
533
+ :refs="compositeItemRefs"
534
+ :props="compositeItemProps"
535
+ :state-attributes-mapping="stateAttributesMapping"
536
+ >
537
+ <slot v-bind="slotProps" />
538
+ </CompositeItem>
539
+ <slot v-else-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
540
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
541
+ <slot :state="state" />
542
+ </component>
543
+ <input v-bind="inputProps">
544
+ </template>
@@ -0,0 +1,18 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import type { RadioRootState } from './RadioRoot.vue'
3
+ import { inject } from 'vue'
4
+
5
+ export type RadioRootContext = Readonly<Ref<RadioRootState>>
6
+
7
+ export const radioRootContextKey: InjectionKey<RadioRootContext> = Symbol('RadioRootContext')
8
+
9
+ export function useRadioRootContext() {
10
+ const context = inject(radioRootContextKey, undefined)
11
+ if (!context) {
12
+ throw new Error(
13
+ 'Base UI Vue: RadioRootContext is missing. Radio parts must be placed within <RadioRoot>.',
14
+ )
15
+ }
16
+
17
+ return context
18
+ }
@@ -0,0 +1,46 @@
1
+ export enum RadioRootDataAttributes {
2
+ /**
3
+ * Present when the radio is checked.
4
+ */
5
+ checked = 'data-checked',
6
+ /**
7
+ * Present when the radio is not checked.
8
+ */
9
+ unchecked = 'data-unchecked',
10
+ /**
11
+ * Present when the radio is disabled.
12
+ */
13
+ disabled = 'data-disabled',
14
+ /**
15
+ * Present when the radio is readonly.
16
+ */
17
+ readonly = 'data-readonly',
18
+ /**
19
+ * Present when the radio is required.
20
+ */
21
+ required = 'data-required',
22
+ /**
23
+ * Present when the radio is in a valid state.
24
+ */
25
+ valid = 'data-valid',
26
+ /**
27
+ * Present when the radio is in an invalid state.
28
+ */
29
+ invalid = 'data-invalid',
30
+ /**
31
+ * Present when the radio has been touched.
32
+ */
33
+ touched = 'data-touched',
34
+ /**
35
+ * Present when the radio's value has changed.
36
+ */
37
+ dirty = 'data-dirty',
38
+ /**
39
+ * Present when the radio field has a value.
40
+ */
41
+ filled = 'data-filled',
42
+ /**
43
+ * Present when the radio is focused.
44
+ */
45
+ focused = 'data-focused',
46
+ }
@@ -0,0 +1,29 @@
1
+ import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
2
+ import type { TransitionStatus } from '../../utils/useTransitionStatus'
3
+ import { fieldValidityMapping } from '../../field/utils/constants'
4
+ import { transitionStatusMapping } from '../../utils/stateAttributesMapping'
5
+ import { RadioRootDataAttributes } from '../root/RadioRootDataAttributes'
6
+
7
+ const CHECKED_ATTRS: Record<string, string> = {
8
+ [RadioRootDataAttributes.checked]: '',
9
+ }
10
+
11
+ const UNCHECKED_ATTRS: Record<string, string> = {
12
+ [RadioRootDataAttributes.unchecked]: '',
13
+ }
14
+
15
+ export const stateAttributesMapping = {
16
+ checked(value): Record<string, string> {
17
+ if (value) {
18
+ return CHECKED_ATTRS
19
+ }
20
+
21
+ return UNCHECKED_ATTRS
22
+ },
23
+ ...transitionStatusMapping,
24
+ ...fieldValidityMapping,
25
+ } satisfies StateAttributesMapping<{
26
+ checked: boolean
27
+ transitionStatus: TransitionStatus
28
+ valid: boolean | null
29
+ }>