base-ui-vue 0.1.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 (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/button/Button.cjs +524 -0
  4. package/dist/button/Button.cjs.map +1 -0
  5. package/dist/button/Button.js +453 -0
  6. package/dist/button/Button.js.map +1 -0
  7. package/dist/composite/composite.cjs +56 -0
  8. package/dist/composite/composite.cjs.map +1 -0
  9. package/dist/composite/composite.js +21 -0
  10. package/dist/composite/composite.js.map +1 -0
  11. package/dist/control/FieldControl.cjs +576 -0
  12. package/dist/control/FieldControl.cjs.map +1 -0
  13. package/dist/control/FieldControl.js +511 -0
  14. package/dist/control/FieldControl.js.map +1 -0
  15. package/dist/control/FieldControlDataAttributes.cjs +42 -0
  16. package/dist/control/FieldControlDataAttributes.cjs.map +1 -0
  17. package/dist/control/FieldControlDataAttributes.js +36 -0
  18. package/dist/control/FieldControlDataAttributes.js.map +1 -0
  19. package/dist/description/FieldDescription.cjs +86 -0
  20. package/dist/description/FieldDescription.cjs.map +1 -0
  21. package/dist/description/FieldDescription.js +81 -0
  22. package/dist/description/FieldDescription.js.map +1 -0
  23. package/dist/direction-provider/DirectionContext.cjs +26 -0
  24. package/dist/direction-provider/DirectionContext.cjs.map +1 -0
  25. package/dist/direction-provider/DirectionContext.js +15 -0
  26. package/dist/direction-provider/DirectionContext.js.map +1 -0
  27. package/dist/direction-provider/DirectionProvider.cjs +37 -0
  28. package/dist/direction-provider/DirectionProvider.cjs.map +1 -0
  29. package/dist/direction-provider/DirectionProvider.js +32 -0
  30. package/dist/direction-provider/DirectionProvider.js.map +1 -0
  31. package/dist/error/FieldError.cjs +414 -0
  32. package/dist/error/FieldError.cjs.map +1 -0
  33. package/dist/error/FieldError.js +373 -0
  34. package/dist/error/FieldError.js.map +1 -0
  35. package/dist/fallback/AvatarFallback.cjs +165 -0
  36. package/dist/fallback/AvatarFallback.cjs.map +1 -0
  37. package/dist/fallback/AvatarFallback.js +136 -0
  38. package/dist/fallback/AvatarFallback.js.map +1 -0
  39. package/dist/form/Form.cjs +159 -0
  40. package/dist/form/Form.cjs.map +1 -0
  41. package/dist/form/Form.js +154 -0
  42. package/dist/form/Form.js.map +1 -0
  43. package/dist/header/AccordionHeader.cjs +189 -0
  44. package/dist/header/AccordionHeader.cjs.map +1 -0
  45. package/dist/header/AccordionHeader.js +148 -0
  46. package/dist/header/AccordionHeader.js.map +1 -0
  47. package/dist/image/AvatarImage.cjs +150 -0
  48. package/dist/image/AvatarImage.cjs.map +1 -0
  49. package/dist/image/AvatarImage.js +145 -0
  50. package/dist/image/AvatarImage.js.map +1 -0
  51. package/dist/image/AvatarImageDataAttributes.cjs +26 -0
  52. package/dist/image/AvatarImageDataAttributes.cjs.map +1 -0
  53. package/dist/image/AvatarImageDataAttributes.js +20 -0
  54. package/dist/image/AvatarImageDataAttributes.js.map +1 -0
  55. package/dist/index.cjs +64 -0
  56. package/dist/index.d.cts +1501 -0
  57. package/dist/index.d.cts.map +1 -0
  58. package/dist/index.d.ts +1501 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +15 -0
  61. package/dist/index2.cjs +2767 -0
  62. package/dist/index2.cjs.map +1 -0
  63. package/dist/index2.js +2618 -0
  64. package/dist/index2.js.map +1 -0
  65. package/package.json +77 -0
  66. package/src/accordion/accordion.types.ts +126 -0
  67. package/src/accordion/header/AccordionHeader.vue +36 -0
  68. package/src/accordion/index.ts +10 -0
  69. package/src/accordion/item/AccordionItem.vue +124 -0
  70. package/src/accordion/item/AccordionItemContext.ts +24 -0
  71. package/src/accordion/item/AccordionItemDataAttributes.ts +15 -0
  72. package/src/accordion/item/stateAttributesMapping.ts +14 -0
  73. package/src/accordion/panel/AccordionPanel.vue +156 -0
  74. package/src/accordion/panel/AccordionPanelCssVars.ts +12 -0
  75. package/src/accordion/root/AccordionRoot.vue +130 -0
  76. package/src/accordion/root/AccordionRootContext.ts +37 -0
  77. package/src/accordion/root/AccordionRootDataAttributes.ts +10 -0
  78. package/src/accordion/root/stateAttributesMapping.ts +6 -0
  79. package/src/accordion/trigger/AccordionTrigger.vue +186 -0
  80. package/src/avatar/fallback/AvatarFallback.vue +75 -0
  81. package/src/avatar/image/AvatarImage.vue +103 -0
  82. package/src/avatar/image/AvatarImageDataAttributes.ts +14 -0
  83. package/src/avatar/image/useImageLoadingStatus.ts +58 -0
  84. package/src/avatar/index.ts +19 -0
  85. package/src/avatar/root/AvatarRoot.vue +62 -0
  86. package/src/avatar/root/AvatarRootContext.ts +22 -0
  87. package/src/avatar/root/stateAttributesMapping.ts +7 -0
  88. package/src/button/Button.vue +59 -0
  89. package/src/button/ButtonDataAttributes.ts +6 -0
  90. package/src/button/button.types.ts +22 -0
  91. package/src/button/index.ts +2 -0
  92. package/src/collapsible/collapsible.types.ts +64 -0
  93. package/src/collapsible/index.ts +6 -0
  94. package/src/collapsible/panel/CollapsiblePanel.vue +145 -0
  95. package/src/collapsible/panel/CollapsiblePanelCssVars.ts +12 -0
  96. package/src/collapsible/panel/CollapsiblePanelDataAttributes.ts +18 -0
  97. package/src/collapsible/panel/useCollapsiblePanel.ts +489 -0
  98. package/src/collapsible/root/CollapsibleRoot.vue +60 -0
  99. package/src/collapsible/root/CollapsibleRootContext.ts +18 -0
  100. package/src/collapsible/root/stateAttributesMapping.ts +9 -0
  101. package/src/collapsible/root/useCollapsibleRoot.ts +252 -0
  102. package/src/collapsible/trigger/CollapsibleTrigger.vue +63 -0
  103. package/src/collapsible/trigger/CollapsibleTriggerDataAttributes.ts +6 -0
  104. package/src/composite/composite.ts +232 -0
  105. package/src/composite/constants.ts +1 -0
  106. package/src/composite/item/CompositeItem.vue +75 -0
  107. package/src/composite/item/useCompositeItem.ts +63 -0
  108. package/src/composite/list/CompositeList.vue +168 -0
  109. package/src/composite/list/CompositeListContext.ts +21 -0
  110. package/src/composite/list/useCompositeListItem.ts +130 -0
  111. package/src/composite/root/CompositeRoot.vue +106 -0
  112. package/src/composite/root/CompositeRootContext.ts +36 -0
  113. package/src/composite/root/index.ts +7 -0
  114. package/src/composite/root/useCompositeRoot.ts +418 -0
  115. package/src/direction-provider/DirectionContext.ts +29 -0
  116. package/src/direction-provider/DirectionProvider.vue +31 -0
  117. package/src/direction-provider/index.ts +8 -0
  118. package/src/field/control/FieldControl.vue +211 -0
  119. package/src/field/control/FieldControlDataAttributes.ts +30 -0
  120. package/src/field/description/FieldDescription.vue +62 -0
  121. package/src/field/description/FieldDescriptionDataAttributes.ts +30 -0
  122. package/src/field/error/FieldError.vue +159 -0
  123. package/src/field/error/FieldErrorDataAttributes.ts +38 -0
  124. package/src/field/index.ts +27 -0
  125. package/src/field/item/FieldItem.vue +63 -0
  126. package/src/field/item/FieldItemContext.ts +16 -0
  127. package/src/field/label/FieldLabel.vue +102 -0
  128. package/src/field/label/FieldLabelDataAttributes.ts +30 -0
  129. package/src/field/root/FieldRoot.vue +262 -0
  130. package/src/field/root/FieldRootContext.ts +97 -0
  131. package/src/field/root/FieldRootDataAttributes.ts +30 -0
  132. package/src/field/root/useFieldRootState.ts +81 -0
  133. package/src/field/root/useFieldValidation.ts +298 -0
  134. package/src/field/root/useFieldValidity.ts +30 -0
  135. package/src/field/useField.ts +73 -0
  136. package/src/field/utils/constants.ts +45 -0
  137. package/src/field/utils/getCombinedFieldValidityData.ts +18 -0
  138. package/src/field/validity/FieldValidity.vue +36 -0
  139. package/src/fieldset/index.ts +8 -0
  140. package/src/fieldset/legend/FieldsetLegend.vue +72 -0
  141. package/src/fieldset/root/FieldsetRoot.vue +74 -0
  142. package/src/fieldset/root/FieldsetRootContext.ts +26 -0
  143. package/src/floating-ui-vue/types.ts +4 -0
  144. package/src/floating-ui-vue/utils/composite.ts +475 -0
  145. package/src/floating-ui-vue/utils/constants.ts +4 -0
  146. package/src/floating-ui-vue/utils/event.ts +4 -0
  147. package/src/floating-ui-vue/utils.ts +2 -0
  148. package/src/form/Form.vue +188 -0
  149. package/src/form/FormContext.ts +59 -0
  150. package/src/form/index.ts +10 -0
  151. package/src/index.ts +14 -0
  152. package/src/labelable-provider/LabelableContext.ts +33 -0
  153. package/src/labelable-provider/LabelableProvider.vue +55 -0
  154. package/src/labelable-provider/index.ts +6 -0
  155. package/src/labelable-provider/useAriaLabelledBy.ts +100 -0
  156. package/src/labelable-provider/useLabelableId.ts +30 -0
  157. package/src/merge-props/index.ts +1 -0
  158. package/src/merge-props/mergeProps.ts +192 -0
  159. package/src/test/index.ts +1 -0
  160. package/src/test/utils.ts +9 -0
  161. package/src/types/index.ts +10 -0
  162. package/src/use-button/index.ts +1 -0
  163. package/src/use-button/useButton.ts +231 -0
  164. package/src/use-render/index.ts +1 -0
  165. package/src/use-render/useRender.spec.ts +90 -0
  166. package/src/use-render/useRender.ts +152 -0
  167. package/src/utils/collapsibleOpenStateMapping.ts +33 -0
  168. package/src/utils/constants.ts +1 -0
  169. package/src/utils/createBaseUIEventDetails.ts +127 -0
  170. package/src/utils/empty.ts +5 -0
  171. package/src/utils/error.ts +19 -0
  172. package/src/utils/getStateAttributesProps.ts +31 -0
  173. package/src/utils/isElementDisabled.ts +7 -0
  174. package/src/utils/noop.ts +1 -0
  175. package/src/utils/reasons.ts +69 -0
  176. package/src/utils/resolveRef.ts +9 -0
  177. package/src/utils/slot.ts +6 -0
  178. package/src/utils/stateAttributesMapping.ts +28 -0
  179. package/src/utils/transitionStatusMapping.ts +22 -0
  180. package/src/utils/types.ts +47 -0
  181. package/src/utils/useAnimationFrame.ts +130 -0
  182. package/src/utils/useAnimationsFinished.ts +101 -0
  183. package/src/utils/useBaseUiId.ts +9 -0
  184. package/src/utils/useControllableState.ts +44 -0
  185. package/src/utils/useFocusableWhenDisabled.ts +85 -0
  186. package/src/utils/useId.ts +26 -0
  187. package/src/utils/useMergedRefs.ts +91 -0
  188. package/src/utils/useOpenChangeComplete.ts +52 -0
  189. package/src/utils/useRenderElement.ts +162 -0
  190. package/src/utils/useTimeout.ts +48 -0
  191. package/src/utils/useTransitionStatus.ts +104 -0
  192. package/src/utils/warn.ts +15 -0
@@ -0,0 +1,418 @@
1
+ import type { Ref } from 'vue'
2
+ import type { TextDirection } from '../../direction-provider/DirectionContext'
3
+ import type { HTMLProps } from '../../types'
4
+ import type { Dimensions, ModifierKey } from '../composite'
5
+ import type { CompositeMetadata } from '../list/CompositeList.vue'
6
+ import { computed, ref } from 'vue'
7
+ import { isElementDisabled } from '../../utils/isElementDisabled'
8
+ import { useMergedRefs } from '../../utils/useMergedRefs'
9
+ import {
10
+ ALL_KEYS,
11
+ ARROW_DOWN,
12
+ ARROW_KEYS,
13
+ ARROW_LEFT,
14
+ ARROW_RIGHT,
15
+ ARROW_UP,
16
+ createGridCellMap,
17
+ END,
18
+ findNonDisabledListIndex,
19
+ getGridCellIndexOfCorner,
20
+ getGridCellIndices,
21
+ getGridNavigatedIndex,
22
+ getMaxListIndex,
23
+ getMinListIndex,
24
+ HOME,
25
+ HORIZONTAL_KEYS,
26
+ HORIZONTAL_KEYS_WITH_EXTRA_KEYS,
27
+ isIndexOutOfListBounds,
28
+ isListIndexDisabled,
29
+ isNativeInput,
30
+ MODIFIER_KEYS,
31
+ scrollIntoViewIfNeeded,
32
+ VERTICAL_KEYS,
33
+ VERTICAL_KEYS_WITH_EXTRA_KEYS,
34
+ } from '../composite'
35
+ import { ACTIVE_COMPOSITE_ITEM } from '../constants'
36
+
37
+ export interface UseCompositeRootParameters {
38
+ orientation?: () => 'horizontal' | 'vertical' | 'both' | undefined
39
+ cols?: () => number | undefined
40
+ loopFocus?: () => boolean | undefined
41
+ highlightedIndex?: () => number | undefined
42
+ defaultHighlightedIndex?: () => number | undefined
43
+ onHighlightedIndexChange?: (index: number) => void
44
+ dense?: () => boolean | undefined
45
+ direction: () => TextDirection
46
+ itemSizes?: () => Array<Dimensions> | undefined
47
+ rootRef?: Ref<HTMLElement | null>
48
+ /**
49
+ * When `true`, pressing the Home key moves focus to the first item,
50
+ * and pressing the End key moves focus to the last item.
51
+ * @default false
52
+ */
53
+ enableHomeAndEndKeys?: () => boolean | undefined
54
+ /**
55
+ * When `true`, keypress events on Composite's navigation keys
56
+ * be stopped with event.stopPropagation().
57
+ * @default false
58
+ */
59
+ stopEventPropagation?: () => boolean | undefined
60
+ /**
61
+ * Array of item indices to be considered disabled.
62
+ * Used for composite items that are focusable when disabled.
63
+ */
64
+ disabledIndices?: () => number[] | undefined
65
+ /**
66
+ * Array of [modifier key values](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#modifier_keys) that should allow normal keyboard actions
67
+ * when pressed. By default, all modifier keys prevent normal actions.
68
+ * @default []
69
+ */
70
+ modifierKeys?: () => ModifierKey[] | undefined
71
+ }
72
+
73
+ const EMPTY_ARRAY: never[] = []
74
+
75
+ export function useCompositeRoot(params: UseCompositeRootParameters) {
76
+ const orientation = computed(() => params.orientation?.() ?? 'both')
77
+ const cols = computed(() => params.cols?.() ?? 1)
78
+ const loopFocus = computed(() => params.loopFocus?.() ?? true)
79
+ const dense = computed(() => params.dense?.() ?? false)
80
+ const itemSizes = computed(() => params.itemSizes?.())
81
+ const enableHomeAndEndKeys = computed(
82
+ () => params.enableHomeAndEndKeys?.() ?? false,
83
+ )
84
+ const stopEventPropagation = computed(
85
+ () => params.stopEventPropagation?.() ?? false,
86
+ )
87
+ const disabledIndices = computed(
88
+ () => params.disabledIndices?.() ?? EMPTY_ARRAY,
89
+ )
90
+ const modifierKeys = computed(
91
+ () => params.modifierKeys?.() ?? (EMPTY_ARRAY as ModifierKey[]),
92
+ )
93
+
94
+ const externalHighlightedIndex = computed(() => params.highlightedIndex?.())
95
+ const externalSetHighlightedIndex = params.onHighlightedIndexChange
96
+
97
+ const internalHighlightedIndex = ref(params.defaultHighlightedIndex?.() ?? 0)
98
+ const isGrid = computed(() => cols.value > 1)
99
+
100
+ const rootRef = ref<HTMLElement | null>(null)
101
+ const mergedRef = useMergedRefs(rootRef, params.rootRef)
102
+ const elementsRef = ref<Array<HTMLElement | null>>([])
103
+ let hasSetDefaultIndex = false
104
+
105
+ const highlightedIndex = computed(
106
+ () => externalHighlightedIndex.value ?? internalHighlightedIndex.value,
107
+ )
108
+
109
+ function onHighlightedIndexChange(
110
+ index: number,
111
+ shouldScrollIntoView = false,
112
+ ) {
113
+ if (externalHighlightedIndex.value !== undefined) {
114
+ if (externalSetHighlightedIndex) {
115
+ externalSetHighlightedIndex(index)
116
+ }
117
+ }
118
+ else {
119
+ internalHighlightedIndex.value = index
120
+ if (externalSetHighlightedIndex) {
121
+ externalSetHighlightedIndex(index)
122
+ }
123
+ }
124
+ if (shouldScrollIntoView) {
125
+ const newActiveItem = elementsRef.value[index]
126
+ scrollIntoViewIfNeeded(
127
+ rootRef.value,
128
+ newActiveItem,
129
+ params.direction(),
130
+ orientation.value,
131
+ )
132
+ }
133
+ }
134
+
135
+ function onMapChange(map: Map<Element, CompositeMetadata<any>>) {
136
+ if (map.size === 0 || hasSetDefaultIndex) {
137
+ return
138
+ }
139
+ hasSetDefaultIndex = true
140
+ const sortedElements = Array.from(map.keys())
141
+ const activeItem = (sortedElements.find(compositeElement =>
142
+ compositeElement?.hasAttribute(ACTIVE_COMPOSITE_ITEM),
143
+ ) ?? null) as HTMLElement | null
144
+ // Set the default highlighted index of an arbitrary composite item.
145
+ const activeIndex = activeItem ? sortedElements.indexOf(activeItem) : -1
146
+
147
+ if (activeIndex !== -1) {
148
+ onHighlightedIndexChange(activeIndex)
149
+ }
150
+
151
+ scrollIntoViewIfNeeded(
152
+ rootRef.value,
153
+ activeItem,
154
+ params.direction(),
155
+ orientation.value,
156
+ )
157
+ }
158
+
159
+ function handleFocus(event: FocusEvent) {
160
+ const element = rootRef.value
161
+ if (!element || !isNativeInput(event.target as EventTarget)) {
162
+ return
163
+ }
164
+ const target = event.target as HTMLInputElement
165
+ target.setSelectionRange(0, target.value.length ?? 0)
166
+ }
167
+
168
+ function handleKeydown(event: KeyboardEvent) {
169
+ const RELEVANT_KEYS = enableHomeAndEndKeys.value ? ALL_KEYS : ARROW_KEYS
170
+ if (!RELEVANT_KEYS.has(event.key)) {
171
+ return
172
+ }
173
+
174
+ if (isModifierKeySet(event, modifierKeys.value)) {
175
+ return
176
+ }
177
+
178
+ const element = rootRef.value
179
+ if (!element) {
180
+ return
181
+ }
182
+ const isRtl = params.direction() === 'rtl'
183
+
184
+ const horizontalForwardKey = isRtl ? ARROW_LEFT : ARROW_RIGHT
185
+ const forwardKey = {
186
+ horizontal: horizontalForwardKey,
187
+ vertical: ARROW_DOWN,
188
+ both: horizontalForwardKey,
189
+ }[orientation.value]
190
+
191
+ const horizontalBackwardKey = isRtl ? ARROW_RIGHT : ARROW_LEFT
192
+ const backwardKey = {
193
+ horizontal: horizontalBackwardKey,
194
+ vertical: ARROW_UP,
195
+ both: horizontalBackwardKey,
196
+ }[orientation.value]
197
+
198
+ if (
199
+ isNativeInput(event.target as EventTarget)
200
+ && !isElementDisabled(event.target as HTMLElement)
201
+ ) {
202
+ const target = event.target as HTMLInputElement
203
+ const selectionStart = target.selectionStart
204
+ const selectionEnd = target.selectionEnd
205
+ const textContent = target.value ?? ''
206
+ // return to native textbox behavior when
207
+ // 1 - Shift is held to make a text selection, or if there already is a text selection
208
+ if (
209
+ selectionStart == null
210
+ || event.shiftKey
211
+ || selectionStart !== selectionEnd
212
+ ) {
213
+ return
214
+ }
215
+ // 2 - arrow-ing forward and not in the last position of the text
216
+ if (event.key !== backwardKey && selectionStart < textContent.length) {
217
+ return
218
+ }
219
+ // 3 - arrow-ing backward and not in the first position of the text
220
+ if (event.key !== forwardKey && selectionStart > 0) {
221
+ return
222
+ }
223
+ }
224
+
225
+ let nextIndex = highlightedIndex.value
226
+ const minIndex = getMinListIndex(elementsRef, disabledIndices.value)
227
+ const maxIndex = getMaxListIndex(elementsRef, disabledIndices.value)
228
+
229
+ if (isGrid.value) {
230
+ const sizes
231
+ = itemSizes.value
232
+ || Array.from({ length: elementsRef.value.length }, () => ({
233
+ width: 1,
234
+ height: 1,
235
+ }))
236
+ // To calculate movements on the grid, we use hypothetical cell indices
237
+ // as if every item was 1x1, then convert back to real indices.
238
+ const cellMap = createGridCellMap(sizes, cols.value, dense.value)
239
+ const minGridIndex = cellMap.findIndex(
240
+ index =>
241
+ index != null
242
+ && !isListIndexDisabled(elementsRef, index, disabledIndices.value),
243
+ )
244
+ // last enabled index
245
+ const maxGridIndex = cellMap.reduce(
246
+ (foundIndex: number, index, cellIndex) =>
247
+ index != null
248
+ && !isListIndexDisabled(elementsRef, index, disabledIndices.value)
249
+ ? cellIndex
250
+ : foundIndex,
251
+ -1,
252
+ )
253
+
254
+ nextIndex = cellMap[
255
+ getGridNavigatedIndex(elementsRef, {
256
+ event,
257
+ orientation: orientation.value,
258
+ loopFocus: loopFocus.value,
259
+ cols: cols.value,
260
+ // treat undefined (empty grid spaces) as disabled indices so we
261
+ // don't end up in them
262
+ disabledIndices: getGridCellIndices(
263
+ [
264
+ ...((disabledIndices.value.length > 0
265
+ ? disabledIndices.value
266
+ : undefined)
267
+ || elementsRef.value.map((_, index) =>
268
+ isListIndexDisabled(elementsRef, index) ? index : undefined,
269
+ )),
270
+ undefined,
271
+ ],
272
+ cellMap,
273
+ ),
274
+ minIndex: minGridIndex,
275
+ maxIndex: maxGridIndex,
276
+ prevIndex: getGridCellIndexOfCorner(
277
+ highlightedIndex.value > maxIndex
278
+ ? minIndex
279
+ : highlightedIndex.value,
280
+ sizes,
281
+ cellMap,
282
+ cols.value,
283
+ // use a corner matching the edge closest to the direction we're
284
+ // moving in so we don't end up in the same item. Prefer
285
+ // top/left over bottom/right.
286
+
287
+ event.key === ARROW_DOWN
288
+ ? 'bl'
289
+ : event.key === ARROW_RIGHT
290
+ ? 'tr'
291
+ : 'tl',
292
+ ),
293
+ rtl: isRtl,
294
+ })
295
+ ] as number // navigated cell will never be nullish
296
+ }
297
+
298
+ const forwardKeys = {
299
+ horizontal: [horizontalForwardKey],
300
+ vertical: [ARROW_DOWN],
301
+ both: [horizontalForwardKey, ARROW_DOWN],
302
+ }[orientation.value]
303
+
304
+ const backwardKeys = {
305
+ horizontal: [horizontalBackwardKey],
306
+ vertical: [ARROW_UP],
307
+ both: [horizontalBackwardKey, ARROW_UP],
308
+ }[orientation.value]
309
+
310
+ const preventedKeys = isGrid.value
311
+ ? RELEVANT_KEYS
312
+ : ({
313
+ horizontal: enableHomeAndEndKeys.value
314
+ ? HORIZONTAL_KEYS_WITH_EXTRA_KEYS
315
+ : HORIZONTAL_KEYS,
316
+ vertical: enableHomeAndEndKeys.value
317
+ ? VERTICAL_KEYS_WITH_EXTRA_KEYS
318
+ : VERTICAL_KEYS,
319
+ both: RELEVANT_KEYS,
320
+ }[orientation.value] as typeof RELEVANT_KEYS)
321
+
322
+ if (enableHomeAndEndKeys.value) {
323
+ if (event.key === HOME) {
324
+ nextIndex = minIndex
325
+ }
326
+ else if (event.key === END) {
327
+ nextIndex = maxIndex
328
+ }
329
+ }
330
+
331
+ if (
332
+ nextIndex === highlightedIndex.value
333
+ && (forwardKeys.includes(event.key) || backwardKeys.includes(event.key))
334
+ ) {
335
+ if (
336
+ loopFocus.value
337
+ && nextIndex === maxIndex
338
+ && forwardKeys.includes(event.key)
339
+ ) {
340
+ nextIndex = minIndex
341
+ }
342
+ else if (
343
+ loopFocus.value
344
+ && nextIndex === minIndex
345
+ && backwardKeys.includes(event.key)
346
+ ) {
347
+ nextIndex = maxIndex
348
+ }
349
+ else {
350
+ nextIndex = findNonDisabledListIndex(elementsRef as any, {
351
+ startingIndex: nextIndex,
352
+ decrement: backwardKeys.includes(event.key),
353
+ disabledIndices: disabledIndices.value,
354
+ })
355
+ }
356
+ }
357
+
358
+ if (
359
+ nextIndex !== highlightedIndex.value
360
+ && !isIndexOutOfListBounds(elementsRef, nextIndex)
361
+ ) {
362
+ if (stopEventPropagation.value) {
363
+ event.stopPropagation()
364
+ }
365
+
366
+ if (preventedKeys.has(event.key)) {
367
+ event.preventDefault()
368
+ }
369
+ onHighlightedIndexChange(nextIndex, true)
370
+
371
+ // Wait for FocusManager `returnFocus` to execute.
372
+ queueMicrotask(() => {
373
+ elementsRef.value[nextIndex]?.focus()
374
+ })
375
+ }
376
+ }
377
+
378
+ return {
379
+ getRootProps: (externalProps: Record<string, any> = {}): HTMLProps => ({
380
+ ...externalProps,
381
+ 'aria-orientation':
382
+ orientation.value === 'both' ? undefined : orientation.value,
383
+ onFocus(event: FocusEvent) {
384
+ externalProps.onFocus?.(event)
385
+ if (!event.defaultPrevented)
386
+ handleFocus(event)
387
+ },
388
+ onKeydown(event: KeyboardEvent) {
389
+ externalProps.onKeydown?.(event)
390
+ if (!event.defaultPrevented)
391
+ handleKeydown(event)
392
+ },
393
+ }),
394
+ rootRef,
395
+ mergedRef,
396
+ highlightedIndex,
397
+ onHighlightedIndexChange,
398
+ elementsRef,
399
+ disabledIndices,
400
+ onMapChange,
401
+ relayKeyboardEvent: handleKeydown,
402
+ }
403
+ }
404
+
405
+ function isModifierKeySet(
406
+ event: KeyboardEvent,
407
+ ignoredModifierKeys: ModifierKey[],
408
+ ) {
409
+ for (const key of MODIFIER_KEYS.values()) {
410
+ if (ignoredModifierKeys.includes(key as any)) {
411
+ continue
412
+ }
413
+ if (event.getModifierState(key)) {
414
+ return true
415
+ }
416
+ }
417
+ return false
418
+ }
@@ -0,0 +1,29 @@
1
+ import type { ComputedRef, InjectionKey, Ref } from 'vue'
2
+ import { computed, inject } from 'vue'
3
+
4
+ export type TextDirection = 'ltr' | 'rtl'
5
+
6
+ export interface DirectionContextValue {
7
+ direction: Ref<TextDirection>
8
+ }
9
+
10
+ /**
11
+ * @internal
12
+ */
13
+ export const directionContextKey = Symbol(
14
+ 'DirectionContext',
15
+ ) as InjectionKey<DirectionContextValue>
16
+
17
+ export interface DirectionProviderProps {
18
+ /**
19
+ * The reading direction of the text
20
+ * @default 'ltr'
21
+ */
22
+ direction?: TextDirection
23
+ }
24
+
25
+ export function useDirection(): ComputedRef<TextDirection> {
26
+ const context = inject(directionContextKey, undefined)
27
+
28
+ return computed(() => context?.direction.value ?? 'ltr')
29
+ }
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import type { DirectionProviderProps } from './DirectionContext'
3
+ import { provide, toRef } from 'vue'
4
+ import {
5
+ directionContextKey,
6
+
7
+ } from './DirectionContext'
8
+
9
+ /**
10
+ * Enables RTL behavior for Base UI Vue components.
11
+ *
12
+ * Documentation: [Base UI Vue Direction Provider](https://base-ui-vue.com/utils/direction-provider)
13
+ */
14
+ defineOptions({
15
+ name: 'DirectionProvider',
16
+ })
17
+
18
+ const props = withDefaults(defineProps<DirectionProviderProps>(), {
19
+ direction: 'ltr',
20
+ })
21
+
22
+ const contextValue = {
23
+ direction: toRef(props, 'direction'),
24
+ }
25
+
26
+ provide(directionContextKey, contextValue)
27
+ </script>
28
+
29
+ <template>
30
+ <slot />
31
+ </template>
@@ -0,0 +1,8 @@
1
+ export { useDirection } from './DirectionContext'
2
+
3
+ export type {
4
+ DirectionProviderProps,
5
+ TextDirection,
6
+ } from './DirectionContext'
7
+
8
+ export { default as DirectionProvider } from './DirectionProvider.vue'
@@ -0,0 +1,211 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import type { FieldRootState } from '../root/FieldRoot.vue'
4
+ import { computed, ref, shallowRef, useAttrs, watchEffect } from 'vue'
5
+ import { useLabelableContext } from '../../labelable-provider/LabelableContext'
6
+ import { useAriaLabelledBy } from '../../labelable-provider/useAriaLabelledBy'
7
+ import { useLabelableId } from '../../labelable-provider/useLabelableId'
8
+ import { mergeProps } from '../../merge-props/mergeProps'
9
+ import { useRenderElement } from '../../utils/useRenderElement'
10
+ import { useFieldRootContext } from '../root/FieldRootContext'
11
+ import { useField } from '../useField'
12
+ import { fieldValidityMapping } from '../utils/constants'
13
+
14
+ export type FieldControlState = FieldRootState
15
+
16
+ export interface FieldControlProps extends BaseUIComponentProps<FieldControlState> {
17
+ id?: string
18
+ name?: string
19
+ disabled?: boolean
20
+ value?: string
21
+ defaultValue?: string
22
+ autofocus?: boolean
23
+ type?: string
24
+ required?: boolean
25
+ pattern?: string
26
+ minlength?: number
27
+ maxlength?: number
28
+ min?: string | number
29
+ max?: string | number
30
+ step?: string | number
31
+ placeholder?: string
32
+ }
33
+
34
+ defineOptions({
35
+ name: 'FieldControl',
36
+ inheritAttrs: false,
37
+ })
38
+
39
+ const props = withDefaults(defineProps<FieldControlProps>(), {
40
+ as: 'input',
41
+ disabled: false,
42
+ autofocus: false,
43
+ })
44
+
45
+ const emit = defineEmits<{
46
+ valueChange: [value: string, event: Event]
47
+ }>()
48
+
49
+ const attrs = useAttrs()
50
+ const attrsObject = attrs as Record<string, any>
51
+
52
+ const fieldContext = useFieldRootContext()
53
+
54
+ const {
55
+ state: fieldState,
56
+ name: fieldName,
57
+ disabled: fieldDisabled,
58
+ setTouched,
59
+ setDirty,
60
+ markedDirtyRef,
61
+ validityData,
62
+ setFocused,
63
+ setFilled,
64
+ validationMode,
65
+ validation,
66
+ } = fieldContext
67
+
68
+ const disabled = computed(() => fieldDisabled.value || props.disabled)
69
+ const name = computed(() => fieldName.value ?? props.name)
70
+
71
+ const state = computed<FieldControlState>(() => ({
72
+ ...fieldState.value,
73
+ disabled: disabled.value,
74
+ }))
75
+
76
+ const controlId = useLabelableId({ id: props.id })
77
+
78
+ const isControlled = computed(() => props.value !== undefined)
79
+ const internalValue = shallowRef(props.defaultValue ?? '')
80
+
81
+ const currentValue = computed(() => {
82
+ if (isControlled.value)
83
+ return props.value!
84
+ return internalValue.value
85
+ })
86
+
87
+ const inputElementRef = ref<HTMLElement | null>(null)
88
+
89
+ const { labelId } = useLabelableContext()
90
+ const ariaLabelledByAttr = computed(() => attrs['aria-labelledby'] as string | undefined)
91
+ const ariaLabelledBy = useAriaLabelledBy({
92
+ ariaLabelledBy: ariaLabelledByAttr,
93
+ labelId,
94
+ labelSourceRef: inputElementRef,
95
+ enableFallback: true,
96
+ labelSourceId: controlId.value ?? undefined,
97
+ })
98
+
99
+ watchEffect(() => {
100
+ validation.setInputRef(inputElementRef.value as HTMLInputElement | null)
101
+ })
102
+
103
+ useField({
104
+ id: controlId,
105
+ name,
106
+ commit: (v: unknown) => validation.commit(v),
107
+ value: currentValue,
108
+ getValue: () => (inputElementRef.value as HTMLInputElement)?.value ?? internalValue.value,
109
+ controlRef: inputElementRef,
110
+ })
111
+
112
+ watchEffect(() => {
113
+ const el = inputElementRef.value as HTMLInputElement | null
114
+ if (!el)
115
+ return
116
+
117
+ if (el.value || (isControlled.value && props.value !== '')) {
118
+ setFilled(true)
119
+ }
120
+ else if (isControlled.value && props.value === '') {
121
+ setFilled(false)
122
+ }
123
+ })
124
+
125
+ function handleInput(event: Event) {
126
+ const target = event.target as HTMLInputElement
127
+ internalValue.value = target.value
128
+ emit('valueChange', target.value, event)
129
+ setDirty(target.value !== validityData.value.initialValue)
130
+ setFilled(target.value !== '')
131
+ }
132
+
133
+ function handleInputCombined(event: Event) {
134
+ handleInput(event)
135
+ const inputValidationProps = validation.getInputValidationProps()
136
+ inputValidationProps.onInput?.(event)
137
+ }
138
+
139
+ function handleFocus() {
140
+ setFocused(true)
141
+ }
142
+
143
+ function handleBlur(event: Event) {
144
+ const target = event.target as HTMLInputElement
145
+ setTouched(true)
146
+ setFocused(false)
147
+
148
+ if (validationMode.value === 'onBlur') {
149
+ validation.commit(target.value)
150
+ }
151
+ }
152
+
153
+ function handleKeydown(event: KeyboardEvent) {
154
+ const target = event.target as HTMLInputElement
155
+ if (target.tagName === 'INPUT' && event.key === 'Enter') {
156
+ setTouched(true)
157
+ markedDirtyRef.value = true
158
+ validation.commit(target.value)
159
+ }
160
+ }
161
+
162
+ const controlProps = computed(() => mergeProps(
163
+ attrsObject,
164
+ validation.getInputValidationProps(),
165
+ {
166
+ 'id': controlId.value,
167
+ 'disabled': disabled.value,
168
+ 'name': name.value,
169
+ 'aria-labelledby': ariaLabelledBy.value,
170
+ 'autofocus': props.autofocus || undefined,
171
+ 'type': props.type,
172
+ 'required': props.required,
173
+ 'pattern': props.pattern,
174
+ 'minlength': props.minlength,
175
+ 'maxlength': props.maxlength,
176
+ 'min': props.min,
177
+ 'max': props.max,
178
+ 'step': props.step,
179
+ 'placeholder': props.placeholder,
180
+ 'value': isControlled.value ? props.value : internalValue.value,
181
+ 'onInput': handleInputCombined,
182
+ 'onFocus': handleFocus,
183
+ 'onBlur': handleBlur,
184
+ 'onKeydown': handleKeydown,
185
+ },
186
+ ))
187
+
188
+ const {
189
+ tag,
190
+ mergedProps,
191
+ renderless,
192
+ ref: renderRef,
193
+ } = useRenderElement({
194
+ componentProps: props,
195
+ state,
196
+ props: controlProps,
197
+ stateAttributesMapping: fieldValidityMapping,
198
+ defaultTagName: 'input',
199
+ ref: inputElementRef,
200
+ })
201
+ </script>
202
+
203
+ <template>
204
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
205
+ <component
206
+ :is="tag"
207
+ v-else
208
+ :ref="renderRef"
209
+ v-bind="mergedProps"
210
+ />
211
+ </template>