base-ui-vue 0.1.0 → 0.2.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 (204) hide show
  1. package/README.md +41 -1
  2. package/dist/button/Button.cjs +53 -12
  3. package/dist/button/Button.cjs.map +1 -1
  4. package/dist/button/Button.js +26 -15
  5. package/dist/button/Button.js.map +1 -1
  6. package/dist/button/ToolbarButton.cjs +367 -0
  7. package/dist/button/ToolbarButton.cjs.map +1 -0
  8. package/dist/button/ToolbarButton.js +320 -0
  9. package/dist/button/ToolbarButton.js.map +1 -0
  10. package/dist/button/ToolbarButtonDataAttributes.cjs +27 -0
  11. package/dist/button/ToolbarButtonDataAttributes.cjs.map +1 -0
  12. package/dist/button/ToolbarButtonDataAttributes.js +21 -0
  13. package/dist/button/ToolbarButtonDataAttributes.js.map +1 -0
  14. package/dist/checkbox/index.cjs +1173 -0
  15. package/dist/checkbox/index.cjs.map +1 -0
  16. package/dist/checkbox/index.js +1048 -0
  17. package/dist/checkbox/index.js.map +1 -0
  18. package/dist/checkbox-group/CheckboxGroup.cjs +629 -0
  19. package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -0
  20. package/dist/checkbox-group/CheckboxGroup.js +540 -0
  21. package/dist/checkbox-group/CheckboxGroup.js.map +1 -0
  22. package/dist/checkbox-group/CheckboxGroupDataAttributes.cjs +18 -0
  23. package/dist/checkbox-group/CheckboxGroupDataAttributes.cjs.map +1 -0
  24. package/dist/checkbox-group/CheckboxGroupDataAttributes.js +12 -0
  25. package/dist/checkbox-group/CheckboxGroupDataAttributes.js.map +1 -0
  26. package/dist/composite/composite.cjs +167 -0
  27. package/dist/composite/composite.cjs.map +1 -1
  28. package/dist/composite/composite.js +96 -1
  29. package/dist/composite/composite.js.map +1 -1
  30. package/dist/composite/constants.cjs +12 -0
  31. package/dist/composite/constants.cjs.map +1 -0
  32. package/dist/composite/constants.js +6 -0
  33. package/dist/composite/constants.js.map +1 -0
  34. package/dist/control/FieldControl.cjs +18 -343
  35. package/dist/control/FieldControl.cjs.map +1 -1
  36. package/dist/control/FieldControl.js +14 -285
  37. package/dist/control/FieldControl.js.map +1 -1
  38. package/dist/control/SliderControl.cjs +636 -0
  39. package/dist/control/SliderControl.cjs.map +1 -0
  40. package/dist/control/SliderControl.js +553 -0
  41. package/dist/control/SliderControl.js.map +1 -0
  42. package/dist/control/SliderControlDataAttributes.cjs +47 -0
  43. package/dist/control/SliderControlDataAttributes.cjs.map +1 -0
  44. package/dist/control/SliderControlDataAttributes.js +41 -0
  45. package/dist/control/SliderControlDataAttributes.js.map +1 -0
  46. package/dist/csp-provider/CSPContext.cjs +32 -0
  47. package/dist/csp-provider/CSPContext.cjs.map +1 -0
  48. package/dist/csp-provider/CSPContext.js +21 -0
  49. package/dist/csp-provider/CSPContext.js.map +1 -0
  50. package/dist/csp-provider/CSPProvider.cjs +46 -0
  51. package/dist/csp-provider/CSPProvider.cjs.map +1 -0
  52. package/dist/csp-provider/CSPProvider.js +41 -0
  53. package/dist/csp-provider/CSPProvider.js.map +1 -0
  54. package/dist/description/FieldDescription.cjs +5 -5
  55. package/dist/description/FieldDescription.cjs.map +1 -1
  56. package/dist/description/FieldDescription.js +1 -1
  57. package/dist/direction-provider/DirectionProvider.cjs +2 -2
  58. package/dist/direction-provider/DirectionProvider.cjs.map +1 -1
  59. package/dist/direction-provider/DirectionProvider.js +1 -1
  60. package/dist/error/FieldError.cjs +10 -288
  61. package/dist/error/FieldError.cjs.map +1 -1
  62. package/dist/error/FieldError.js +4 -246
  63. package/dist/error/FieldError.js.map +1 -1
  64. package/dist/form/Form.cjs +5 -4
  65. package/dist/form/Form.cjs.map +1 -1
  66. package/dist/form/Form.js +5 -4
  67. package/dist/form/Form.js.map +1 -1
  68. package/dist/group/ToolbarGroup.cjs +92 -0
  69. package/dist/group/ToolbarGroup.cjs.map +1 -0
  70. package/dist/group/ToolbarGroup.js +87 -0
  71. package/dist/group/ToolbarGroup.js.map +1 -0
  72. package/dist/group/ToolbarGroupDataAttributes.cjs +23 -0
  73. package/dist/group/ToolbarGroupDataAttributes.cjs.map +1 -0
  74. package/dist/group/ToolbarGroupDataAttributes.js +17 -0
  75. package/dist/group/ToolbarGroupDataAttributes.js.map +1 -0
  76. package/dist/header/AccordionHeader.cjs +2 -2
  77. package/dist/header/AccordionHeader.js +1 -1
  78. package/dist/image/AvatarImage.cjs +4 -4
  79. package/dist/image/AvatarImage.cjs.map +1 -1
  80. package/dist/image/AvatarImage.js +1 -1
  81. package/dist/index.cjs +80 -10
  82. package/dist/index.d.cts +2751 -612
  83. package/dist/index.d.cts.map +1 -1
  84. package/dist/index.d.ts +2751 -612
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +15 -5
  87. package/dist/index2.cjs +3651 -370
  88. package/dist/index2.cjs.map +1 -1
  89. package/dist/index2.js +3365 -270
  90. package/dist/index2.js.map +1 -1
  91. package/package.json +8 -4
  92. package/src/accordion/root/AccordionRoot.vue +2 -1
  93. package/src/checkbox/index.ts +23 -0
  94. package/src/checkbox/indicator/CheckboxIndicator.vue +102 -0
  95. package/src/checkbox/indicator/CheckboxIndicatorDataAttributes.ts +61 -0
  96. package/src/checkbox/root/CheckboxRoot.vue +632 -0
  97. package/src/checkbox/root/CheckboxRootContext.ts +22 -0
  98. package/src/checkbox/root/CheckboxRootDataAttributes.ts +54 -0
  99. package/src/checkbox/utils/useStateAttributesMapping.ts +30 -0
  100. package/src/checkbox-group/CheckboxGroup.vue +241 -0
  101. package/src/checkbox-group/CheckboxGroupContext.ts +39 -0
  102. package/src/checkbox-group/CheckboxGroupDataAttributes.ts +6 -0
  103. package/src/checkbox-group/index.ts +11 -0
  104. package/src/checkbox-group/useCheckboxGroupParent.ts +173 -0
  105. package/src/collapsible/panel/useCollapsiblePanel.ts +2 -1
  106. package/src/collapsible/root/useCollapsibleRoot.ts +3 -1
  107. package/src/composite/composite.ts +2 -0
  108. package/src/composite/item/CompositeItem.vue +7 -8
  109. package/src/composite/root/CompositeRoot.vue +12 -1
  110. package/src/csp-provider/CSPContext.ts +26 -0
  111. package/src/csp-provider/CSPProvider.vue +40 -0
  112. package/src/csp-provider/index.ts +5 -0
  113. package/src/field/item/FieldItem.vue +6 -1
  114. package/src/field/label/FieldLabel.vue +10 -51
  115. package/src/field/root/FieldRoot.vue +16 -3
  116. package/src/floating-ui-vue/types.ts +1 -4
  117. package/src/floating-ui-vue/utils/element.ts +12 -0
  118. package/src/floating-ui-vue/utils/shadowDom.ts +44 -0
  119. package/src/floating-ui-vue/utils.ts +3 -0
  120. package/src/form/Form.vue +5 -3
  121. package/src/index.ts +9 -0
  122. package/src/labelable-provider/LabelableContext.ts +2 -2
  123. package/src/labelable-provider/LabelableProvider.vue +21 -4
  124. package/src/labelable-provider/index.ts +2 -0
  125. package/src/labelable-provider/useAriaLabelledBy.ts +9 -9
  126. package/src/labelable-provider/useLabel.ts +115 -0
  127. package/src/labelable-provider/useLabelableId.ts +12 -10
  128. package/src/separator/Separator.vue +65 -0
  129. package/src/separator/SeparatorDataAttributes.ts +7 -0
  130. package/src/separator/index.ts +3 -0
  131. package/src/slider/control/SliderControl.vue +497 -0
  132. package/src/slider/control/SliderControlDataAttributes.ts +35 -0
  133. package/src/slider/index.ts +35 -0
  134. package/src/slider/indicator/SliderIndicator.vue +144 -0
  135. package/src/slider/indicator/SliderIndicatorDataAttributes.ts +35 -0
  136. package/src/slider/label/SliderLabel.vue +75 -0
  137. package/src/slider/root/SliderRoot.vue +557 -0
  138. package/src/slider/root/SliderRootContext.ts +126 -0
  139. package/src/slider/root/SliderRootDataAttributes.ts +35 -0
  140. package/src/slider/root/stateAttributesMapping.ts +13 -0
  141. package/src/slider/thumb/SliderThumb.vue +601 -0
  142. package/src/slider/thumb/SliderThumbDataAttributes.ts +39 -0
  143. package/src/slider/thumb/prehydrationScript.min.ts +5 -0
  144. package/src/slider/thumb/prehydrationScript.template.js +69 -0
  145. package/src/slider/track/SliderTrack.vue +48 -0
  146. package/src/slider/track/SliderTrackDataAttributes.ts +10 -0
  147. package/src/slider/utils/asc.ts +3 -0
  148. package/src/slider/utils/getMidpoint.ts +9 -0
  149. package/src/slider/utils/getPushedThumbValues.ts +68 -0
  150. package/src/slider/utils/getSliderValue.ts +25 -0
  151. package/src/slider/utils/replaceArrayItemAtIndex.ts +15 -0
  152. package/src/slider/utils/resolveThumbCollision.ts +177 -0
  153. package/src/slider/utils/roundValueToStep.ts +19 -0
  154. package/src/slider/utils/test-utils.ts +25 -0
  155. package/src/slider/utils/validateMinimumDistance.ts +20 -0
  156. package/src/slider/utils/valueArrayToPercentages.ts +10 -0
  157. package/src/slider/value/SliderValue.vue +90 -0
  158. package/src/slider/value/SliderValueDataAttributes.ts +35 -0
  159. package/src/switch/index.ts +14 -0
  160. package/src/switch/root/SwitchRoot.vue +448 -0
  161. package/src/switch/root/SwitchRootContext.ts +22 -0
  162. package/src/switch/root/SwitchRootDataAttributes.ts +46 -0
  163. package/src/switch/stateAttributesMapping.ts +23 -0
  164. package/src/switch/thumb/SwitchThumb.vue +59 -0
  165. package/src/switch/thumb/SwitchThumbDataAttributes.ts +46 -0
  166. package/src/toggle/Toggle.vue +211 -0
  167. package/src/toggle/ToggleDataAttributes.ts +6 -0
  168. package/src/toggle/index.ts +3 -0
  169. package/src/toggle-group/ToggleGroup.vue +224 -0
  170. package/src/toggle-group/ToggleGroupContext.ts +45 -0
  171. package/src/toggle-group/ToggleGroupDataAttributes.ts +15 -0
  172. package/src/toggle-group/index.ts +5 -0
  173. package/src/toolbar/button/ToolbarButton.vue +99 -0
  174. package/src/toolbar/button/ToolbarButtonDataAttributes.ts +15 -0
  175. package/src/toolbar/group/ToolbarGroup.vue +70 -0
  176. package/src/toolbar/group/ToolbarGroupContext.ts +23 -0
  177. package/src/toolbar/group/ToolbarGroupDataAttributes.ts +11 -0
  178. package/src/toolbar/index.ts +27 -0
  179. package/src/toolbar/input/ToolbarInput.vue +114 -0
  180. package/src/toolbar/input/ToolbarInputDataAttributes.ts +15 -0
  181. package/src/toolbar/link/ToolbarLink.vue +54 -0
  182. package/src/toolbar/link/ToolbarLinkDataAttributes.ts +7 -0
  183. package/src/toolbar/root/ToolbarRoot.vue +144 -0
  184. package/src/toolbar/root/ToolbarRootContext.ts +29 -0
  185. package/src/toolbar/root/ToolbarRootDataAttributes.ts +11 -0
  186. package/src/toolbar/separator/ToolbarSeparator.vue +41 -0
  187. package/src/toolbar/separator/ToolbarSeparatorDataAttributes.ts +7 -0
  188. package/src/use-button/useButton.ts +2 -1
  189. package/src/utils/areArraysEqual.ts +12 -0
  190. package/src/utils/clamp.ts +7 -0
  191. package/src/utils/createBaseUIEventDetails.ts +9 -0
  192. package/src/utils/formatNumber.ts +7 -0
  193. package/src/utils/owner.ts +5 -0
  194. package/src/utils/resolveAriaLabelledBy.ts +10 -0
  195. package/src/utils/useControllableState.ts +78 -14
  196. package/src/utils/useFocusableWhenDisabled.ts +6 -1
  197. package/src/utils/useMergedRefs.ts +26 -2
  198. package/src/utils/useRegisteredLabelId.ts +21 -0
  199. package/src/utils/valueToPercent.ts +7 -0
  200. package/src/utils/visuallyHidden.ts +24 -0
  201. package/dist/direction-provider/DirectionContext.cjs +0 -26
  202. package/dist/direction-provider/DirectionContext.cjs.map +0 -1
  203. package/dist/direction-provider/DirectionContext.js +0 -15
  204. package/dist/direction-provider/DirectionContext.js.map +0 -1
@@ -0,0 +1,632 @@
1
+ <script setup lang="ts">
2
+ import type { FieldRootState } from '../../field/root/FieldRoot.vue'
3
+ import type { BaseUIChangeEventDetails } from '../../utils/createBaseUIEventDetails'
4
+ import type { BaseUIComponentProps, NonNativeButtonProps } from '../../utils/types'
5
+ import { computed, getCurrentInstance, provide, ref, useAttrs, watch, watchEffect } from 'vue'
6
+ import { useCheckboxGroupContext } from '../../checkbox-group/CheckboxGroupContext'
7
+ import { useFieldItemContext } from '../../field/item/FieldItemContext'
8
+ import { useFieldRootContext } from '../../field/root/FieldRootContext'
9
+ import { useField } from '../../field/useField'
10
+ import { useFormContext } from '../../form/FormContext'
11
+ import { useLabelableContext } from '../../labelable-provider/LabelableContext'
12
+ import { useAriaLabelledBy } from '../../labelable-provider/useAriaLabelledBy'
13
+ import { mergeProps } from '../../merge-props/mergeProps'
14
+ import { useButton } from '../../use-button'
15
+ import { createChangeEventDetails } from '../../utils/createBaseUIEventDetails'
16
+ import { EMPTY_OBJECT } from '../../utils/empty'
17
+ import { REASONS } from '../../utils/reasons'
18
+ import { useBaseUiId } from '../../utils/useBaseUiId'
19
+ import { useControllableState } from '../../utils/useControllableState'
20
+ import { useMergedRefs } from '../../utils/useMergedRefs'
21
+ import { useRenderElement } from '../../utils/useRenderElement'
22
+ import { visuallyHidden, visuallyHiddenInput } from '../../utils/visuallyHidden'
23
+ import { useCheckboxStateAttributesMapping } from '../utils/useStateAttributesMapping'
24
+ import { checkboxRootContextKey } from './CheckboxRootContext'
25
+
26
+ interface GroupCheckboxProps {
27
+ 'id'?: string
28
+ 'checked'?: boolean
29
+ 'indeterminate'?: boolean
30
+ 'aria-controls'?: string
31
+ 'onCheckedChange'?: (
32
+ checked: boolean,
33
+ eventDetails: BaseUIChangeEventDetails<typeof REASONS.none>,
34
+ ) => void
35
+ }
36
+
37
+ export interface CheckboxRootState extends FieldRootState {
38
+ /**
39
+ * Whether the checkbox is currently ticked.
40
+ */
41
+ checked: boolean
42
+ /**
43
+ * Whether the component should ignore user interaction.
44
+ */
45
+ disabled: boolean
46
+ /**
47
+ * Whether the user should be unable to tick or untick the checkbox.
48
+ */
49
+ readOnly: boolean
50
+ /**
51
+ * Whether the user must tick the checkbox before submitting a form.
52
+ */
53
+ required: boolean
54
+ /**
55
+ * Whether the checkbox is in a mixed state: neither ticked, nor unticked.
56
+ */
57
+ indeterminate: boolean
58
+ }
59
+
60
+ export interface CheckboxRootProps
61
+ extends
62
+ NonNativeButtonProps,
63
+ BaseUIComponentProps<CheckboxRootState> {
64
+ /**
65
+ * The id of the input element.
66
+ */
67
+ 'id'?: string
68
+ /**
69
+ * Identifies the field when a form is submitted.
70
+ * @default undefined
71
+ */
72
+ 'name'?: string
73
+ /**
74
+ * Identifies the form that owns the hidden input.
75
+ * Useful when the checkbox is rendered outside the form.
76
+ */
77
+ 'form'?: string
78
+ /**
79
+ * Whether the checkbox is currently ticked.
80
+ *
81
+ * To render an uncontrolled checkbox, use the `defaultChecked` prop instead.
82
+ * @default undefined
83
+ */
84
+ 'checked'?: boolean
85
+ /**
86
+ * Whether the checkbox is initially ticked.
87
+ *
88
+ * To render a controlled checkbox, use the `checked` prop instead.
89
+ * @default false
90
+ */
91
+ 'defaultChecked'?: boolean
92
+ /**
93
+ * Whether the component should ignore user interaction.
94
+ * @default false
95
+ */
96
+ 'disabled'?: boolean
97
+ /**
98
+ * Whether the user should be unable to tick or untick the checkbox.
99
+ * @default false
100
+ */
101
+ 'readOnly'?: boolean
102
+ /**
103
+ * Whether the user must tick the checkbox before submitting a form.
104
+ * @default false
105
+ */
106
+ 'required'?: boolean
107
+ /**
108
+ * Whether the checkbox is in a mixed state: neither ticked, nor unticked.
109
+ * @default false
110
+ */
111
+ 'indeterminate'?: boolean
112
+ /**
113
+ * A ref to access the hidden `<input>` element.
114
+ */
115
+ 'inputRef'?: any
116
+ /**
117
+ * Whether the checkbox controls a group of child checkboxes.
118
+ *
119
+ * Must be used in a [Checkbox Group](https://baseui-vue.com/docs/components/checkbox-group).
120
+ * @default false
121
+ */
122
+ 'parent'?: boolean
123
+ /**
124
+ * The value submitted with the form when the checkbox is unchecked.
125
+ * By default, unchecked checkboxes do not submit any value, matching native checkbox behavior.
126
+ */
127
+ 'uncheckedValue'?: string
128
+ /**
129
+ * The value of the selected checkbox.
130
+ */
131
+ 'value'?: string
132
+ // eslint-disable-next-line vue/prop-name-casing
133
+ 'aria-labelledby'?: string
134
+ }
135
+
136
+ /**
137
+ * Represents the checkbox itself.
138
+ * Renders a `<span>` element and a hidden `<input>` beside.
139
+ *
140
+ * Documentation: [Base UI Vue Checkbox](https://baseui-vue.com/docs/components/checkbox)
141
+ */
142
+ defineOptions({
143
+ name: 'CheckboxRoot',
144
+ inheritAttrs: false,
145
+ })
146
+
147
+ const props = withDefaults(defineProps<CheckboxRootProps>(), {
148
+ as: 'span',
149
+ defaultChecked: false,
150
+ disabled: false,
151
+ readOnly: false,
152
+ required: false,
153
+ indeterminate: false,
154
+ parent: false,
155
+ nativeButton: false,
156
+ })
157
+
158
+ const emit = defineEmits<{
159
+ /**
160
+ * Event handler called when the checkbox is ticked or unticked.
161
+ */
162
+ checkedChange: [
163
+ checked: boolean,
164
+ eventDetails: BaseUIChangeEventDetails<typeof REASONS.none>,
165
+ ]
166
+ }>()
167
+
168
+ const attrs = useAttrs()
169
+ const attrsObject = attrs as Record<string, any>
170
+ const instance = getCurrentInstance()
171
+ const controlSource = Symbol('checkbox-control')
172
+
173
+ const { clearErrors } = useFormContext()
174
+ const {
175
+ disabled: rootDisabled,
176
+ name: fieldName,
177
+ setDirty,
178
+ setFilled,
179
+ setFocused,
180
+ setTouched,
181
+ state: fieldState,
182
+ validationMode,
183
+ validityData,
184
+ shouldValidateOnChange,
185
+ validation: localValidation,
186
+ } = useFieldRootContext()
187
+ const fieldItemContext = useFieldItemContext()
188
+ const labelableContext = useLabelableContext()
189
+ const groupContext = useCheckboxGroupContext(true)
190
+
191
+ const parentContext = computed(() => groupContext?.parent)
192
+ const isGroupedWithParent = computed(() =>
193
+ Boolean(parentContext.value && groupContext && groupContext.allValues.value !== undefined),
194
+ )
195
+
196
+ const disabled = computed(() =>
197
+ rootDisabled.value
198
+ || fieldItemContext.disabled.value
199
+ || groupContext?.disabled.value
200
+ || props.disabled,
201
+ )
202
+ const name = computed(() => fieldName.value ?? props.name)
203
+ const checkboxValue = computed(() => (props.value !== undefined ? props.value : name.value))
204
+
205
+ const rootElementId = useBaseUiId()
206
+ const generatedInputId = useBaseUiId()
207
+ const groupParentId = useBaseUiId()
208
+
209
+ const inputId = computed(() => {
210
+ // Parent/child checkboxes inside a group share a predictable id scheme so the
211
+ // parent can identify itself. Child controls register their real input ids.
212
+ if (isGroupedWithParent.value && parentContext.value) {
213
+ if (props.parent) {
214
+ return parentContext.value.id ?? groupParentId
215
+ }
216
+ }
217
+
218
+ if (props.id) {
219
+ return props.id
220
+ }
221
+
222
+ return labelableContext.controlId.value ?? generatedInputId
223
+ })
224
+
225
+ watchEffect((onCleanup) => {
226
+ // Register the focusable/native control id with the surrounding label system.
227
+ labelableContext.registerControlId(controlSource, inputId.value)
228
+
229
+ onCleanup(() => {
230
+ labelableContext.registerControlId(controlSource, undefined)
231
+ })
232
+ })
233
+
234
+ watch(
235
+ () => ({
236
+ groupedWithParent: isGroupedWithParent.value,
237
+ checkboxValue: checkboxValue.value,
238
+ inputId: inputId.value,
239
+ isParent: props.parent,
240
+ }),
241
+ (nextState, _prevState, onCleanup) => {
242
+ if (
243
+ !nextState.groupedWithParent
244
+ || !groupContext
245
+ || nextState.isParent
246
+ || nextState.checkboxValue === undefined
247
+ ) {
248
+ return
249
+ }
250
+
251
+ groupContext.parent.registerChildControlId(nextState.checkboxValue, nextState.inputId)
252
+
253
+ onCleanup(() => {
254
+ groupContext.parent.registerChildControlId(nextState.checkboxValue!, undefined)
255
+ })
256
+ },
257
+ { immediate: true },
258
+ )
259
+
260
+ const groupProps = computed<GroupCheckboxProps>(() => {
261
+ if (!isGroupedWithParent.value || !groupContext) {
262
+ return {}
263
+ }
264
+
265
+ if (props.parent) {
266
+ return groupContext.parent.getParentProps()
267
+ }
268
+
269
+ if (checkboxValue.value !== undefined) {
270
+ return groupContext.parent.getChildProps(checkboxValue.value)
271
+ }
272
+
273
+ return {}
274
+ })
275
+
276
+ const hasCheckedProp = computed(() =>
277
+ Boolean(instance?.vnode.props && Object.prototype.hasOwnProperty.call(instance.vnode.props, 'checked')),
278
+ )
279
+
280
+ const groupChecked = computed(() => {
281
+ const checked = groupProps.value.checked
282
+ return checked === undefined
283
+ ? (hasCheckedProp.value ? props.checked : undefined)
284
+ : checked
285
+ })
286
+
287
+ const groupIndeterminate = computed(() => {
288
+ const indeterminate = groupProps.value.indeterminate
289
+ return indeterminate === undefined ? props.indeterminate : indeterminate
290
+ })
291
+
292
+ const validation = groupContext?.validation ?? localValidation
293
+
294
+ const { value: checked, setValue: setCheckedState } = useControllableState<boolean>({
295
+ controlled: () => {
296
+ // Child checkboxes in a group derive checked state from the shared value array.
297
+ if (groupContext && !props.parent && checkboxValue.value !== undefined) {
298
+ return groupContext.value.value.includes(checkboxValue.value)
299
+ }
300
+
301
+ return groupChecked.value
302
+ },
303
+ default:
304
+ () => (
305
+ groupContext && !props.parent && checkboxValue.value !== undefined
306
+ ? groupContext.defaultValue.value.includes(checkboxValue.value)
307
+ : props.defaultChecked
308
+ ),
309
+ name: 'CheckboxRoot',
310
+ state: 'checked',
311
+ })
312
+
313
+ const computedChecked = computed(() =>
314
+ isGroupedWithParent.value ? Boolean(groupChecked.value) : checked.value,
315
+ )
316
+ const computedIndeterminate = computed(() =>
317
+ isGroupedWithParent.value
318
+ ? Boolean(groupIndeterminate.value || props.indeterminate)
319
+ : props.indeterminate,
320
+ )
321
+
322
+ const controlRef = ref<HTMLElement | null>(null)
323
+ const inputElementRef = ref<HTMLInputElement | null>(null)
324
+
325
+ useField({
326
+ // Standalone checkboxes participate in Field/Form registration directly.
327
+ // Grouped checkboxes defer validation state to the group container instead.
328
+ enabled: computed(() => !groupContext),
329
+ id: computed(() => rootElementId),
330
+ commit: (value: unknown) => validation.commit(value),
331
+ value: checked,
332
+ controlRef,
333
+ name,
334
+ getValue: () => checked.value,
335
+ })
336
+
337
+ const ariaLabelledBy = useAriaLabelledBy({
338
+ ariaLabelledBy: computed(() => props['aria-labelledby']),
339
+ labelId: labelableContext.labelId,
340
+ // Use the hidden input for native-label fallback detection because it is the
341
+ // element associated with sibling/wrapping `<label>` elements.
342
+ labelSourceRef: inputElementRef,
343
+ enableFallback: !props.nativeButton,
344
+ labelSourceId: inputId,
345
+ })
346
+
347
+ watchEffect(() => {
348
+ if (inputElementRef.value) {
349
+ inputElementRef.value.indeterminate = computedIndeterminate.value
350
+ }
351
+ })
352
+
353
+ watchEffect(() => {
354
+ // Preserve filled field state for initially checked checkboxes without
355
+ // resetting a shared group field back to unfilled.
356
+ if (computedChecked.value) {
357
+ setFilled(true)
358
+ }
359
+ })
360
+
361
+ watchEffect(() => {
362
+ if (!inputElementRef.value) {
363
+ return
364
+ }
365
+
366
+ validation.setInputRef(inputElementRef.value)
367
+ })
368
+
369
+ watch(
370
+ () => checked.value,
371
+ (nextChecked) => {
372
+ if (groupContext) {
373
+ return
374
+ }
375
+
376
+ clearErrors(name.value)
377
+ setFilled(nextChecked)
378
+ setDirty(nextChecked !== validityData.value.initialValue)
379
+
380
+ if (shouldValidateOnChange()) {
381
+ void validation.commit(nextChecked)
382
+ }
383
+ else {
384
+ void validation.commit(nextChecked, true)
385
+ }
386
+ },
387
+ { flush: 'sync' },
388
+ )
389
+
390
+ watch(
391
+ () => ({
392
+ parentContext: parentContext.value,
393
+ checkboxValue: checkboxValue.value,
394
+ disabled: disabled.value,
395
+ }),
396
+ (nextState, _prevState, onCleanup) => {
397
+ if (!nextState.parentContext || nextState.checkboxValue === undefined) {
398
+ return
399
+ }
400
+
401
+ const checkboxKey = nextState.checkboxValue
402
+ const nextDisabledStates = new Map(nextState.parentContext.disabledStatesRef.value)
403
+ nextDisabledStates.set(checkboxKey, nextState.disabled)
404
+ nextState.parentContext.disabledStatesRef.value = nextDisabledStates
405
+
406
+ onCleanup(() => {
407
+ if (!nextState.parentContext) {
408
+ return
409
+ }
410
+
411
+ const cleanupDisabledStates = new Map(nextState.parentContext.disabledStatesRef.value)
412
+ cleanupDisabledStates.delete(checkboxKey)
413
+ nextState.parentContext.disabledStatesRef.value = cleanupDisabledStates
414
+ })
415
+ },
416
+ { immediate: true },
417
+ )
418
+
419
+ const { getButtonProps, buttonRef } = useButton({
420
+ disabled,
421
+ native: computed(() => props.nativeButton),
422
+ })
423
+
424
+ function combineDescriptionProps(
425
+ localProps: Record<string, any>,
426
+ validationProps: Record<string, any>,
427
+ ) {
428
+ const localDescribedBy = localProps['aria-describedby']
429
+ const validationDescribedBy = validationProps['aria-describedby']
430
+ const describedBy = Array.from(
431
+ new Set(
432
+ [localDescribedBy, validationDescribedBy]
433
+ .filter(Boolean)
434
+ .flatMap(value => String(value).split(/\s+/).filter(Boolean)),
435
+ ),
436
+ ).join(' ') || undefined
437
+
438
+ return {
439
+ ...localProps,
440
+ ...validationProps,
441
+ // Avoid duplicating ids when both field descriptions and validation props
442
+ // contribute the same `aria-describedby` tokens.
443
+ 'aria-describedby': describedBy,
444
+ }
445
+ }
446
+
447
+ function applyCheckedChange(
448
+ nextChecked: boolean,
449
+ event: Event,
450
+ onApplied?: () => void,
451
+ ) {
452
+ const details = createChangeEventDetails(REASONS.none, event)
453
+
454
+ emit('checkedChange', nextChecked, details)
455
+
456
+ if (details.isCanceled) {
457
+ return false
458
+ }
459
+
460
+ groupProps.value.onCheckedChange?.(nextChecked, details)
461
+ onApplied?.()
462
+ setCheckedState(nextChecked)
463
+
464
+ // Standalone children of a CheckboxGroup update the shared value array here;
465
+ // parent checkboxes use the group's parent-controller helpers instead.
466
+ if (groupContext && !isGroupedWithParent.value && !props.parent && checkboxValue.value !== undefined) {
467
+ const currentValue = groupContext.value.value.slice()
468
+ const nextValue = nextChecked
469
+ ? Array.from(new Set([...currentValue, checkboxValue.value]))
470
+ : currentValue.filter((item: string) => item !== checkboxValue.value)
471
+
472
+ groupContext.setValue(nextValue, details)
473
+ }
474
+
475
+ return true
476
+ }
477
+
478
+ function handleInputChange(event: Event) {
479
+ const target = event.currentTarget as HTMLInputElement
480
+
481
+ if (props.readOnly || disabled.value || (!props.parent && props.indeterminate && !groupContext)) {
482
+ event.preventDefault()
483
+ event.stopPropagation()
484
+ target.checked = computedChecked.value
485
+ target.indeterminate = computedIndeterminate.value
486
+ return
487
+ }
488
+
489
+ const applied = applyCheckedChange(target.checked, event)
490
+ if (!applied) {
491
+ target.checked = computedChecked.value
492
+ target.indeterminate = computedIndeterminate.value
493
+ }
494
+ }
495
+
496
+ function handleRootClick(event: MouseEvent | KeyboardEvent) {
497
+ if (props.readOnly || disabled.value || (!props.parent && props.indeterminate)) {
498
+ return
499
+ }
500
+
501
+ event.preventDefault()
502
+
503
+ const nextChecked = !computedChecked.value
504
+
505
+ applyCheckedChange(nextChecked, event, () => {
506
+ if (inputElementRef.value) {
507
+ // Keep the hidden native input in sync so form submission mirrors button state.
508
+ inputElementRef.value.checked = nextChecked
509
+ }
510
+ })
511
+ }
512
+
513
+ function handleFocus() {
514
+ setFocused(true)
515
+ }
516
+
517
+ function handleBlur() {
518
+ setTouched(true)
519
+ setFocused(false)
520
+
521
+ if (validationMode.value === 'onBlur') {
522
+ void validation.commit(groupContext ? groupContext.value.value : checked.value)
523
+ }
524
+ }
525
+
526
+ const state = computed<CheckboxRootState>(() => ({
527
+ ...fieldState.value,
528
+ checked: computedChecked.value,
529
+ disabled: disabled.value,
530
+ readOnly: props.readOnly,
531
+ required: props.required,
532
+ indeterminate: computedIndeterminate.value,
533
+ }))
534
+
535
+ provide(checkboxRootContextKey, state)
536
+
537
+ const stateAttributesMapping = useCheckboxStateAttributesMapping(state)
538
+
539
+ const mergedRootRef = useMergedRefs(
540
+ buttonRef,
541
+ controlRef,
542
+ groupContext?.registerControlRef,
543
+ )
544
+
545
+ const rootProps = computed(() => {
546
+ const localDescriptionProps = labelableContext.getDescriptionProps()
547
+ const validationProps = validation.getValidationProps()
548
+ const buttonProps = getButtonProps({
549
+ onClick: handleRootClick,
550
+ })
551
+
552
+ return mergeProps(
553
+ buttonProps,
554
+ combineDescriptionProps(localDescriptionProps, validationProps),
555
+ groupProps.value['aria-controls'] ? { 'aria-controls': groupProps.value['aria-controls'] } : EMPTY_OBJECT,
556
+ {
557
+ 'id': groupProps.value.id ?? (props.nativeButton ? inputId.value : rootElementId),
558
+ 'role': 'checkbox',
559
+ 'aria-checked': computedIndeterminate.value ? 'mixed' : computedChecked.value,
560
+ 'aria-readonly': props.readOnly || undefined,
561
+ 'aria-required': props.required || undefined,
562
+ 'aria-labelledby': ariaLabelledBy.value,
563
+ 'data-parent': props.parent ? '' : undefined,
564
+ 'onFocus': handleFocus,
565
+ 'onBlur': handleBlur,
566
+ },
567
+ attrsObject,
568
+ )
569
+ })
570
+
571
+ const {
572
+ tag,
573
+ mergedProps,
574
+ renderless,
575
+ ref: renderRef,
576
+ } = useRenderElement({
577
+ componentProps: props,
578
+ state,
579
+ props: rootProps,
580
+ stateAttributesMapping,
581
+ defaultTagName: 'span',
582
+ ref: mergedRootRef,
583
+ })
584
+
585
+ const inputProps = computed(() => {
586
+ const localDescriptionProps = labelableContext.getDescriptionProps()
587
+ const validationProps = groupContext
588
+ ? validation.getValidationProps()
589
+ : validation.getInputValidationProps()
590
+
591
+ return mergeProps(
592
+ {
593
+ 'checked': computedChecked.value,
594
+ 'disabled': disabled.value,
595
+ 'form': props.form,
596
+ 'name': props.parent ? undefined : name.value,
597
+ 'id': props.nativeButton ? undefined : inputId.value,
598
+ 'required': props.required,
599
+ 'ref': useMergedRefs(inputElementRef, props.inputRef),
600
+ 'type': 'checkbox',
601
+ 'aria-hidden': true,
602
+ 'tabindex': -1,
603
+ // This hidden input carries the native form semantics while the rendered
604
+ // root element exposes the custom checkbox interaction model.
605
+ 'style': name.value ? visuallyHiddenInput : visuallyHidden,
606
+ 'onChange': handleInputChange,
607
+ onFocus() {
608
+ controlRef.value?.focus()
609
+ },
610
+ },
611
+ checkboxValue.value !== undefined
612
+ ? { value: groupContext ? (computedChecked.value ? checkboxValue.value : '') : checkboxValue.value }
613
+ : EMPTY_OBJECT,
614
+ combineDescriptionProps(localDescriptionProps, validationProps),
615
+ )
616
+ })
617
+ </script>
618
+
619
+ <template>
620
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
621
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
622
+ <slot :state="state" />
623
+ </component>
624
+ <input
625
+ v-if="!computedChecked && !groupContext && name && !parent && uncheckedValue !== undefined"
626
+ type="hidden"
627
+ :form="form"
628
+ :name="name"
629
+ :value="uncheckedValue"
630
+ >
631
+ <input v-bind="inputProps">
632
+ </template>
@@ -0,0 +1,22 @@
1
+ import type { InjectionKey, Ref } from 'vue'
2
+ import type { CheckboxRootState } from './CheckboxRoot.vue'
3
+ import { inject } from 'vue'
4
+
5
+ export type CheckboxRootContext = Readonly<Ref<CheckboxRootState>>
6
+
7
+ export const checkboxRootContextKey: InjectionKey<CheckboxRootContext> = Symbol(
8
+ 'CheckboxRootContext',
9
+ )
10
+
11
+ export function useCheckboxRootContext(optional: true): CheckboxRootContext | undefined
12
+ export function useCheckboxRootContext(optional?: false): CheckboxRootContext
13
+ export function useCheckboxRootContext(optional = false) {
14
+ const context = inject(checkboxRootContextKey, undefined)
15
+ if (!context && !optional) {
16
+ throw new Error(
17
+ 'Base UI Vue: CheckboxRootContext is missing. Checkbox parts must be placed within <CheckboxRoot>.',
18
+ )
19
+ }
20
+
21
+ return context
22
+ }
@@ -0,0 +1,54 @@
1
+ export enum CheckboxRootDataAttributes {
2
+ /**
3
+ * Present when the checkbox is checked.
4
+ */
5
+ checked = 'data-checked',
6
+ /**
7
+ * Present when the checkbox is not checked.
8
+ */
9
+ unchecked = 'data-unchecked',
10
+ /**
11
+ * Present when the checkbox is in an indeterminate state.
12
+ */
13
+ indeterminate = 'data-indeterminate',
14
+ /**
15
+ * Present when the checkbox is disabled.
16
+ */
17
+ disabled = 'data-disabled',
18
+ /**
19
+ * Present when the checkbox is readonly.
20
+ */
21
+ readonly = 'data-readonly',
22
+ /**
23
+ * Present when the checkbox is required.
24
+ */
25
+ required = 'data-required',
26
+ /**
27
+ * Present when the checkbox is in valid state (when wrapped in FieldRoot).
28
+ */
29
+ valid = 'data-valid',
30
+ /**
31
+ * Present when the checkbox is in invalid state (when wrapped in FieldRoot).
32
+ */
33
+ invalid = 'data-invalid',
34
+ /**
35
+ * Present when the checkbox has been touched (when wrapped in FieldRoot).
36
+ */
37
+ touched = 'data-touched',
38
+ /**
39
+ * Present when the checkbox's value has changed (when wrapped in FieldRoot).
40
+ */
41
+ dirty = 'data-dirty',
42
+ /**
43
+ * Present when the checkbox is checked (when wrapped in FieldRoot).
44
+ */
45
+ filled = 'data-filled',
46
+ /**
47
+ * Present when the checkbox is focused (when wrapped in FieldRoot).
48
+ */
49
+ focused = 'data-focused',
50
+ /**
51
+ * Present when the checkbox is acting as the parent checkbox for a group.
52
+ */
53
+ parent = 'data-parent',
54
+ }