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,40 @@
1
+ <script setup lang="ts">
2
+ import type { CSPContextValue } from './CSPContext'
3
+ import { computed, provide, toRef } from 'vue'
4
+ import { cspContextKey } from './CSPContext'
5
+
6
+ export interface CSPProviderProps {
7
+ /**
8
+ * The nonce value to apply to inline `<style>` and `<script>` tags.
9
+ */
10
+ nonce?: string
11
+ /**
12
+ * Whether inline `<style>` elements created by Base UI components should not be rendered.
13
+ * @default false
14
+ */
15
+ disableStyleElements?: boolean
16
+ }
17
+
18
+ /**
19
+ * Provides a default Content Security Policy (CSP) configuration for Base UI Vue components
20
+ * that require inline `<style>` or `<script>` tags.
21
+ */
22
+ defineOptions({
23
+ name: 'CSPProvider',
24
+ })
25
+
26
+ const props = withDefaults(defineProps<CSPProviderProps>(), {
27
+ disableStyleElements: false,
28
+ })
29
+
30
+ const contextValue = computed<CSPContextValue>(() => ({
31
+ nonce: toRef(() => props.nonce),
32
+ disableStyleElements: toRef(() => props.disableStyleElements),
33
+ }))
34
+
35
+ provide(cspContextKey, contextValue.value)
36
+ </script>
37
+
38
+ <template>
39
+ <slot />
40
+ </template>
@@ -0,0 +1,5 @@
1
+ export type { CSPContextValue } from './CSPContext'
2
+
3
+ export { useCSPContext } from './CSPContext'
4
+ export { default as CSPProvider } from './CSPProvider.vue'
5
+ export type { CSPProviderProps } from './CSPProvider.vue'
@@ -3,6 +3,7 @@ import type { BaseUIComponentProps } from '../../utils/types'
3
3
  import type { FieldRootState } from '../root/FieldRoot.vue'
4
4
  import type { FieldItemContext } from './FieldItemContext'
5
5
  import { computed, provide, useAttrs } from 'vue'
6
+ import { useCheckboxGroupContext } from '../../checkbox-group/CheckboxGroupContext'
6
7
  import LabelableProvider from '../../labelable-provider/LabelableProvider.vue'
7
8
  import { useRenderElement } from '../../utils/useRenderElement'
8
9
  import { useFieldRootContext } from '../root/FieldRootContext'
@@ -35,6 +36,10 @@ const attrs = useAttrs()
35
36
  const { state, disabled: rootDisabled } = useFieldRootContext(false)
36
37
 
37
38
  const disabled = computed(() => rootDisabled.value || props.disabled)
39
+ const checkboxGroupContext = useCheckboxGroupContext(true)
40
+ const parentId = computed(() => checkboxGroupContext?.parent.id)
41
+ const hasParentCheckbox = computed(() => checkboxGroupContext?.allValues.value !== undefined)
42
+ const controlId = computed(() => (hasParentCheckbox.value ? parentId.value : undefined))
38
43
 
39
44
  const contextValue: FieldItemContext = {
40
45
  disabled,
@@ -54,7 +59,7 @@ const { tag, mergedProps, renderless } = useRenderElement({
54
59
  </script>
55
60
 
56
61
  <template>
57
- <LabelableProvider>
62
+ <LabelableProvider :control-id="controlId">
58
63
  <slot v-if="renderless" :props="mergedProps" :state="state" />
59
64
  <component :is="tag" v-else v-bind="mergedProps">
60
65
  <slot :state="state" />
@@ -1,9 +1,9 @@
1
1
  <script setup lang="ts">
2
2
  import type { BaseUIComponentProps } from '../../utils/types'
3
3
  import type { FieldRootState } from '../root/FieldRoot.vue'
4
- import { computed, useAttrs, watchEffect } from 'vue'
4
+ import { computed, useAttrs } from 'vue'
5
5
  import { useLabelableContext } from '../../labelable-provider/LabelableContext'
6
- import { useBaseUiId } from '../../utils/useBaseUiId'
6
+ import { useLabel } from '../../labelable-provider/useLabel'
7
7
  import { useRenderElement } from '../../utils/useRenderElement'
8
8
  import { useFieldRootContext } from '../root/FieldRootContext'
9
9
  import { fieldValidityMapping } from '../utils/constants'
@@ -35,60 +35,19 @@ const props = withDefaults(defineProps<FieldLabelProps>(), {
35
35
  const attrs = useAttrs()
36
36
 
37
37
  const fieldRootContext = useFieldRootContext(false)
38
-
39
- const { controlId, setLabelId, labelId: contextLabelId } = useLabelableContext()
40
-
41
- const generatedLabelId = useBaseUiId(props.id)
42
- const labelId = computed(() => props.id ?? contextLabelId.value ?? generatedLabelId)
43
-
44
- watchEffect((onCleanup) => {
45
- const id = labelId.value
46
- setLabelId(id)
47
- onCleanup(() => setLabelId(undefined))
38
+ const { labelId } = useLabelableContext()
39
+ const label = useLabel({
40
+ id: computed(() => labelId.value ?? props.id),
41
+ native: computed(() => props.nativeLabel),
48
42
  })
49
43
 
50
- function handleInteraction(event: MouseEvent) {
51
- const target = event.target as HTMLElement | null
52
- if (target?.closest('button,input,select,textarea')) {
53
- return
54
- }
55
-
56
- if (!event.defaultPrevented && event.detail > 1) {
57
- event.preventDefault()
58
- }
59
-
60
- if (props.nativeLabel || !controlId.value) {
61
- return
62
- }
63
-
64
- const controlElement = document.getElementById(controlId.value)
65
- if (controlElement) {
66
- controlElement.focus()
67
- }
68
- }
69
-
70
44
  const { tag, mergedProps, renderless } = useRenderElement({
71
45
  componentProps: props,
72
46
  state: fieldRootContext.state,
73
- props: computed(() => {
74
- if (props.nativeLabel) {
75
- return {
76
- ...attrs,
77
- id: labelId.value,
78
- for: controlId.value ?? undefined,
79
- onMousedown: handleInteraction,
80
- }
81
- }
82
-
83
- return {
84
- ...attrs,
85
- id: labelId.value,
86
- onClick: handleInteraction,
87
- onPointerdown: (event: PointerEvent) => {
88
- event.preventDefault()
89
- },
90
- }
91
- }),
47
+ props: computed(() => ({
48
+ ...attrs,
49
+ ...label.props.value,
50
+ })),
92
51
  stateAttributesMapping: fieldValidityMapping,
93
52
  defaultTagName: 'label',
94
53
  })
@@ -125,9 +125,22 @@ const parentLabelable = useLabelableContext()
125
125
  const controlId = ref<string | null | undefined>(undefined)
126
126
  const labelId = ref<string | undefined>(undefined)
127
127
  const messageIds = ref<string[]>([])
128
+ const controlIdRegistrations = new Map<symbol, string | null | undefined>()
128
129
 
129
- function setControlId(id: string | null | undefined) {
130
- controlId.value = id
130
+ function registerControlId(source: symbol, id: string | null | undefined) {
131
+ if (id === undefined) {
132
+ controlIdRegistrations.delete(source)
133
+ }
134
+ else {
135
+ controlIdRegistrations.set(source, id)
136
+ }
137
+
138
+ if (controlIdRegistrations.size === 0) {
139
+ controlId.value = undefined
140
+ return
141
+ }
142
+
143
+ controlId.value = controlIdRegistrations.values().next().value
131
144
  }
132
145
 
133
146
  function setLabelId(id: string | undefined) {
@@ -147,7 +160,7 @@ function getDescriptionProps() {
147
160
 
148
161
  const labelableContext: LabelableContext = {
149
162
  controlId,
150
- setControlId,
163
+ registerControlId,
151
164
  labelId,
152
165
  setLabelId,
153
166
  messageIds,
@@ -1,4 +1 @@
1
- export interface Dimensions {
2
- width: number
3
- height: number
4
- }
1
+ export type { Coords, Dimensions } from '@floating-ui/dom'
@@ -0,0 +1,12 @@
1
+ export function matchesFocusVisible(element: Element | null) {
2
+ if (!element) {
3
+ return true
4
+ }
5
+
6
+ try {
7
+ return element.matches(':focus-visible')
8
+ }
9
+ catch {
10
+ return true
11
+ }
12
+ }
@@ -0,0 +1,44 @@
1
+ import { isShadowRoot } from '@floating-ui/utils/dom'
2
+
3
+ export function activeElement(doc: Document) {
4
+ let element = doc.activeElement
5
+
6
+ while (element?.shadowRoot?.activeElement != null) {
7
+ element = element.shadowRoot.activeElement
8
+ }
9
+
10
+ return element
11
+ }
12
+
13
+ export function contains(parent?: Element | null, child?: Element | null) {
14
+ if (!parent || !child) {
15
+ return false
16
+ }
17
+
18
+ const rootNode = child.getRootNode?.()
19
+
20
+ if (parent.contains(child)) {
21
+ return true
22
+ }
23
+
24
+ if (rootNode && isShadowRoot(rootNode)) {
25
+ let next: Element | null = child
26
+ while (next) {
27
+ if (parent === next) {
28
+ return true
29
+ }
30
+
31
+ next = (next.parentNode as Element | null) ?? ((next as unknown as ShadowRoot).host ?? null)
32
+ }
33
+ }
34
+
35
+ return false
36
+ }
37
+
38
+ export function getTarget(event: Event) {
39
+ if ('composedPath' in event && typeof event.composedPath === 'function') {
40
+ return event.composedPath()[0]
41
+ }
42
+
43
+ return (event as Event).target
44
+ }
@@ -1,2 +1,5 @@
1
1
  export * from './utils/composite'
2
+ export * from './utils/constants'
3
+ export * from './utils/element'
2
4
  export * from './utils/event'
5
+ export * from './utils/shadowDom'
package/src/form/Form.vue CHANGED
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { BaseUIComponentProps } from '../utils/types'
3
3
  import type { FormContext, FormErrors, FormField, FormValidationMode } from './FormContext'
4
- import { computed, provide, ref, shallowRef, useAttrs, watch } from 'vue'
4
+ import { computed, nextTick, provide, ref, shallowRef, useAttrs, watch } from 'vue'
5
5
  import { EMPTY_OBJECT } from '../utils/empty'
6
6
  import { useRenderElement } from '../utils/useRenderElement'
7
7
  import { formContextKey } from './FormContext'
@@ -76,12 +76,14 @@ function focusControl(el: HTMLElement | null) {
76
76
  }
77
77
  }
78
78
 
79
- watch(internalErrors, () => {
79
+ watch(internalErrors, async () => {
80
80
  if (!submittedRef.value)
81
81
  return
82
82
 
83
83
  submittedRef.value = false
84
84
 
85
+ await nextTick()
86
+
85
87
  const invalidFields = Array.from(formRef.value.fields.values()).filter(
86
88
  field => field.validityData.state.valid === false,
87
89
  )
@@ -90,7 +92,7 @@ watch(internalErrors, () => {
90
92
  const controlRef = invalidFields[0].controlRef
91
93
  focusControl(controlRef.value)
92
94
  }
93
- })
95
+ }, { flush: 'post' })
94
96
 
95
97
  function handleImperativeValidate(fieldName?: string) {
96
98
  const values = Array.from(formRef.value.fields.values())
package/src/index.ts CHANGED
@@ -4,11 +4,20 @@ export const version = '0.0.0'
4
4
  export * from './accordion'
5
5
  export * from './avatar'
6
6
  export * from './button'
7
+ export * from './checkbox'
8
+ export * from './checkbox-group'
7
9
  export * from './collapsible'
10
+ export * from './csp-provider'
8
11
  export * from './direction-provider'
9
12
  export * from './field'
10
13
  export * from './fieldset'
11
14
  export * from './form'
12
15
  export * from './merge-props'
16
+ export * from './separator'
17
+ export * from './slider'
18
+ export * from './switch'
19
+ export * from './toggle'
20
+ export * from './toggle-group'
21
+ export * from './toolbar'
13
22
  export * from './use-render'
14
23
  export { Slot } from './utils/slot'
@@ -8,7 +8,7 @@ export interface LabelableContext {
8
8
  * When `null` the association is implicit.
9
9
  */
10
10
  controlId: Readonly<Ref<string | null | undefined>>
11
- setControlId: (id: string | null | undefined) => void
11
+ registerControlId: (source: symbol, id: string | null | undefined) => void
12
12
  labelId: Readonly<Ref<string | undefined>>
13
13
  setLabelId: (id: string | undefined) => void
14
14
  messageIds: Readonly<Ref<string[]>>
@@ -18,7 +18,7 @@ export interface LabelableContext {
18
18
 
19
19
  const defaultContext: LabelableContext = {
20
20
  controlId: shallowReadonly(shallowRef<string | null | undefined>(undefined)),
21
- setControlId: NOOP,
21
+ registerControlId: NOOP,
22
22
  labelId: shallowReadonly(shallowRef<string | undefined>(undefined)),
23
23
  setLabelId: NOOP,
24
24
  messageIds: shallowReadonly(shallowRef<string[]>([])),
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import type { LabelableContext } from './LabelableContext'
3
3
  import { provide, ref } from 'vue'
4
+ import { useBaseUiId } from '../utils/useBaseUiId'
4
5
  import { labelableContextKey, useLabelableContext } from './LabelableContext'
5
6
 
6
7
  export interface LabelableProviderProps {
@@ -12,14 +13,30 @@ defineOptions({ name: 'LabelableProvider' })
12
13
 
13
14
  const props = defineProps<LabelableProviderProps>()
14
15
 
15
- const controlId = ref<string | null | undefined>(props.controlId)
16
+ const defaultId = useBaseUiId()
17
+ const initialControlId = props.controlId === undefined ? defaultId : props.controlId
18
+
19
+ const controlId = ref<string | null | undefined>(initialControlId)
16
20
  const labelId = ref<string | undefined>(props.labelId)
17
21
  const messageIds = ref<string[]>([])
22
+ const registrations = new Map<symbol, string | null | undefined>()
18
23
 
19
24
  const parent = useLabelableContext()
20
25
 
21
- function setControlId(id: string | null | undefined) {
22
- controlId.value = id
26
+ function registerControlId(source: symbol, id: string | null | undefined) {
27
+ if (id === undefined) {
28
+ registrations.delete(source)
29
+ }
30
+ else {
31
+ registrations.set(source, id)
32
+ }
33
+
34
+ if (registrations.size === 0) {
35
+ controlId.value = initialControlId
36
+ return
37
+ }
38
+
39
+ controlId.value = registrations.values().next().value
23
40
  }
24
41
 
25
42
  function setLabelId(id: string | undefined) {
@@ -39,7 +56,7 @@ function getDescriptionProps() {
39
56
 
40
57
  const contextValue: LabelableContext = {
41
58
  controlId,
42
- setControlId,
59
+ registerControlId,
43
60
  labelId,
44
61
  setLabelId,
45
62
  messageIds,
@@ -2,5 +2,7 @@ export { labelableContextKey, useLabelableContext } from './LabelableContext'
2
2
  export type { LabelableContext } from './LabelableContext'
3
3
  export { default as LabelableProvider } from './LabelableProvider.vue'
4
4
  export type { LabelableProviderProps } from './LabelableProvider.vue'
5
+ export { useLabel } from './useLabel'
6
+ export type { UseLabelParameters } from './useLabel'
5
7
  export { useLabelableId } from './useLabelableId'
6
8
  export type { UseLabelableIdParameters } from './useLabelableId'
@@ -1,6 +1,5 @@
1
- import type { Ref } from 'vue'
2
- import { computed, ref, watchEffect } from 'vue'
3
- import { useBaseUiId } from '../utils/useBaseUiId'
1
+ import type { MaybeRefOrGetter, Ref } from 'vue'
2
+ import { computed, ref, toValue, watchPostEffect } from 'vue'
4
3
 
5
4
  export interface UseAriaLabelledByParameters {
6
5
  /**
@@ -26,7 +25,7 @@ export interface UseAriaLabelledByParameters {
26
25
  * Optional base id used when generating an id
27
26
  * for an implicit label discovered in the DOM.
28
27
  */
29
- labelSourceId?: string
28
+ labelSourceId?: MaybeRefOrGetter<string | undefined>
30
29
  }
31
30
 
32
31
  export function useAriaLabelledBy(params: UseAriaLabelledByParameters) {
@@ -34,23 +33,24 @@ export function useAriaLabelledBy(params: UseAriaLabelledByParameters) {
34
33
 
35
34
  const fallbackAriaLabelledBy = ref<string | undefined>(undefined)
36
35
 
37
- const generatedLabelId = useBaseUiId(
38
- labelSourceId ? `${labelSourceId}-label` : undefined,
39
- )
36
+ const generatedLabelId = computed(() => {
37
+ const sourceId = toValue(labelSourceId)
38
+ return sourceId ? `${sourceId}-label` : undefined
39
+ })
40
40
 
41
41
  const resolvedAriaLabelledBy = computed(() => {
42
42
  const explicit = ariaLabelledBy?.value
43
43
  const fromContext = labelId?.value
44
44
  return explicit ?? fromContext ?? fallbackAriaLabelledBy.value
45
45
  })
46
- watchEffect(() => {
46
+ watchPostEffect(() => {
47
47
  const explicitOrContext = ariaLabelledBy?.value || labelId?.value
48
48
  if (explicitOrContext || !enableFallback) {
49
49
  fallbackAriaLabelledBy.value = undefined
50
50
  return
51
51
  }
52
52
 
53
- const next = getAriaLabelledBy(labelSourceRef.value, generatedLabelId)
53
+ const next = getAriaLabelledBy(labelSourceRef.value, generatedLabelId.value)
54
54
  if (fallbackAriaLabelledBy.value !== next) {
55
55
  fallbackAriaLabelledBy.value = next
56
56
  }
@@ -0,0 +1,115 @@
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import { isHTMLElement } from '@floating-ui/utils/dom'
3
+ import { computed, toValue } from 'vue'
4
+ import { getTarget } from '../floating-ui-vue/utils'
5
+ import { ownerDocument } from '../utils/owner'
6
+ import { useRegisteredLabelId } from '../utils/useRegisteredLabelId'
7
+ import { useLabelableContext } from './LabelableContext'
8
+
9
+ export interface UseLabelParameters {
10
+ id?: MaybeRefOrGetter<string | undefined>
11
+ /**
12
+ * Control id used when no labelable context control id exists.
13
+ */
14
+ fallbackControlId?: MaybeRefOrGetter<string | null | undefined>
15
+ /**
16
+ * Whether the rendered element is a native `<label>`.
17
+ * @default false
18
+ */
19
+ native?: MaybeRefOrGetter<boolean | undefined>
20
+ /**
21
+ * Additional callback to sync the current label id with local component state/store.
22
+ */
23
+ setLabelId?: ((nextLabelId: string | undefined) => void) | undefined
24
+ /**
25
+ * Custom focus handler for non-native labels.
26
+ * If omitted, focus behavior targets the resolved control id.
27
+ */
28
+ focusControl?:
29
+ | ((event: MouseEvent, controlId: string | null | undefined) => void)
30
+ | undefined
31
+ }
32
+
33
+ export function useLabel(params: UseLabelParameters = {}) {
34
+ const {
35
+ id: idProp,
36
+ fallbackControlId,
37
+ native,
38
+ setLabelId: setLabelIdProp,
39
+ focusControl: focusControlProp,
40
+ } = params
41
+
42
+ const { controlId: contextControlId, setLabelId: setContextLabelId } = useLabelableContext()
43
+
44
+ const syncLabelId = (nextLabelId: string | undefined) => {
45
+ setContextLabelId(nextLabelId)
46
+ setLabelIdProp?.(nextLabelId)
47
+ }
48
+
49
+ const labelId = useRegisteredLabelId(idProp, syncLabelId)
50
+ const resolvedControlId = computed(() => contextControlId.value ?? toValue(fallbackControlId))
51
+ const isNative = computed(() => toValue(native) ?? false)
52
+
53
+ function defaultFocusControl(event: MouseEvent, controlId: string | null | undefined) {
54
+ if (!controlId) {
55
+ return
56
+ }
57
+
58
+ const controlElement = ownerDocument(event.currentTarget as Element)?.getElementById(controlId)
59
+ if (isHTMLElement(controlElement)) {
60
+ focusElementWithVisible(controlElement)
61
+ }
62
+ }
63
+
64
+ function handleInteraction(event: MouseEvent) {
65
+ const target = getTarget(event) as HTMLElement | null
66
+ if (target?.closest('button,input,select,textarea')) {
67
+ return
68
+ }
69
+
70
+ if (!event.defaultPrevented && event.detail > 1) {
71
+ event.preventDefault()
72
+ }
73
+
74
+ if (isNative.value) {
75
+ return
76
+ }
77
+
78
+ if (focusControlProp) {
79
+ focusControlProp(event, resolvedControlId.value)
80
+ return
81
+ }
82
+
83
+ defaultFocusControl(event, resolvedControlId.value)
84
+ }
85
+
86
+ const labelProps = computed(() => {
87
+ if (isNative.value) {
88
+ return {
89
+ id: labelId.value,
90
+ for: resolvedControlId.value ?? undefined,
91
+ onMousedown: handleInteraction,
92
+ }
93
+ }
94
+
95
+ return {
96
+ id: labelId.value,
97
+ onClick: handleInteraction,
98
+ onPointerdown(event: PointerEvent) {
99
+ event.preventDefault()
100
+ },
101
+ }
102
+ })
103
+
104
+ return {
105
+ labelId,
106
+ props: labelProps,
107
+ }
108
+ }
109
+
110
+ export function focusElementWithVisible(element: HTMLElement) {
111
+ element.focus({
112
+ // @ts-expect-error not available in types yet
113
+ focusVisible: true,
114
+ })
115
+ }
@@ -1,30 +1,32 @@
1
- import { computed, watchEffect } from 'vue'
1
+ import type { MaybeRefOrGetter } from 'vue'
2
+ import { computed, toValue, watchEffect } from 'vue'
2
3
  import { useBaseUiId } from '../utils/useBaseUiId'
3
4
  import { useLabelableContext } from './LabelableContext'
4
5
 
5
6
  export interface UseLabelableIdParameters {
6
- id?: string
7
+ id?: MaybeRefOrGetter<string | undefined>
7
8
  }
8
9
 
9
10
  export function useLabelableId(params: UseLabelableIdParameters = {}) {
10
11
  const { id } = params
11
- const { controlId, setControlId } = useLabelableContext()
12
+ const { controlId, registerControlId } = useLabelableContext()
13
+ const source = Symbol('labelable-id')
12
14
 
13
- const defaultId = useBaseUiId(id)
14
- const resolvedId = id ?? defaultId
15
+ const defaultId = useBaseUiId()
16
+ const resolvedId = computed(() => toValue(id) ?? defaultId)
15
17
 
16
18
  watchEffect((onCleanup) => {
17
- if (!resolvedId) {
18
- setControlId(undefined)
19
+ if (!resolvedId.value) {
20
+ registerControlId(source, undefined)
19
21
  return
20
22
  }
21
23
 
22
- setControlId(resolvedId)
24
+ registerControlId(source, resolvedId.value)
23
25
 
24
26
  onCleanup(() => {
25
- setControlId(undefined)
27
+ registerControlId(source, undefined)
26
28
  })
27
29
  })
28
30
 
29
- return computed(() => controlId.value ?? defaultId)
31
+ return computed(() => controlId.value ?? resolvedId.value)
30
32
  }
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps, Orientation } from '../utils/types'
3
+ import { computed, useAttrs } from 'vue'
4
+ import { useRenderElement } from '../utils/useRenderElement'
5
+
6
+ export interface SeparatorState {
7
+ /**
8
+ * The orientation of the separator.
9
+ */
10
+ orientation: Orientation
11
+ }
12
+
13
+ export interface SeparatorProps extends BaseUIComponentProps<SeparatorState> {
14
+ /**
15
+ * The orientation of the separator.
16
+ * @default 'horizontal'
17
+ */
18
+ orientation?: Orientation
19
+ }
20
+
21
+ /**
22
+ * A separator element accessible to screen readers.
23
+ * Renders a `<div>` element.
24
+ *
25
+ * Documentation: [Base UI Vue Separator](https://baseui-vue.com/docs/components/separator)
26
+ */
27
+ defineOptions({
28
+ name: 'Separator',
29
+ inheritAttrs: false,
30
+ })
31
+
32
+ const props = withDefaults(defineProps<SeparatorProps>(), {
33
+ as: 'div',
34
+ orientation: 'horizontal',
35
+ })
36
+
37
+ const attrs = useAttrs()
38
+
39
+ const state = computed<SeparatorState>(() => ({
40
+ orientation: props.orientation,
41
+ }))
42
+
43
+ const {
44
+ tag,
45
+ mergedProps,
46
+ renderless,
47
+ ref: renderRef,
48
+ } = useRenderElement({
49
+ componentProps: props,
50
+ state,
51
+ props: computed(() => ({
52
+ ...attrs,
53
+ 'role': 'separator',
54
+ 'aria-orientation': props.orientation,
55
+ })),
56
+ defaultTagName: 'div',
57
+ })
58
+ </script>
59
+
60
+ <template>
61
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
62
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
63
+ <slot :state="state" />
64
+ </component>
65
+ </template>
@@ -0,0 +1,7 @@
1
+ export enum SeparatorDataAttributes {
2
+ /**
3
+ * Indicates the orientation of the separator.
4
+ * @type {'horizontal' | 'vertical'}
5
+ */
6
+ orientation = 'data-orientation',
7
+ }