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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/button/Button.cjs +524 -0
- package/dist/button/Button.cjs.map +1 -0
- package/dist/button/Button.js +453 -0
- package/dist/button/Button.js.map +1 -0
- package/dist/composite/composite.cjs +56 -0
- package/dist/composite/composite.cjs.map +1 -0
- package/dist/composite/composite.js +21 -0
- package/dist/composite/composite.js.map +1 -0
- package/dist/control/FieldControl.cjs +576 -0
- package/dist/control/FieldControl.cjs.map +1 -0
- package/dist/control/FieldControl.js +511 -0
- package/dist/control/FieldControl.js.map +1 -0
- package/dist/control/FieldControlDataAttributes.cjs +42 -0
- package/dist/control/FieldControlDataAttributes.cjs.map +1 -0
- package/dist/control/FieldControlDataAttributes.js +36 -0
- package/dist/control/FieldControlDataAttributes.js.map +1 -0
- package/dist/description/FieldDescription.cjs +86 -0
- package/dist/description/FieldDescription.cjs.map +1 -0
- package/dist/description/FieldDescription.js +81 -0
- package/dist/description/FieldDescription.js.map +1 -0
- package/dist/direction-provider/DirectionContext.cjs +26 -0
- package/dist/direction-provider/DirectionContext.cjs.map +1 -0
- package/dist/direction-provider/DirectionContext.js +15 -0
- package/dist/direction-provider/DirectionContext.js.map +1 -0
- package/dist/direction-provider/DirectionProvider.cjs +37 -0
- package/dist/direction-provider/DirectionProvider.cjs.map +1 -0
- package/dist/direction-provider/DirectionProvider.js +32 -0
- package/dist/direction-provider/DirectionProvider.js.map +1 -0
- package/dist/error/FieldError.cjs +414 -0
- package/dist/error/FieldError.cjs.map +1 -0
- package/dist/error/FieldError.js +373 -0
- package/dist/error/FieldError.js.map +1 -0
- package/dist/fallback/AvatarFallback.cjs +165 -0
- package/dist/fallback/AvatarFallback.cjs.map +1 -0
- package/dist/fallback/AvatarFallback.js +136 -0
- package/dist/fallback/AvatarFallback.js.map +1 -0
- package/dist/form/Form.cjs +159 -0
- package/dist/form/Form.cjs.map +1 -0
- package/dist/form/Form.js +154 -0
- package/dist/form/Form.js.map +1 -0
- package/dist/header/AccordionHeader.cjs +189 -0
- package/dist/header/AccordionHeader.cjs.map +1 -0
- package/dist/header/AccordionHeader.js +148 -0
- package/dist/header/AccordionHeader.js.map +1 -0
- package/dist/image/AvatarImage.cjs +150 -0
- package/dist/image/AvatarImage.cjs.map +1 -0
- package/dist/image/AvatarImage.js +145 -0
- package/dist/image/AvatarImage.js.map +1 -0
- package/dist/image/AvatarImageDataAttributes.cjs +26 -0
- package/dist/image/AvatarImageDataAttributes.cjs.map +1 -0
- package/dist/image/AvatarImageDataAttributes.js +20 -0
- package/dist/image/AvatarImageDataAttributes.js.map +1 -0
- package/dist/index.cjs +64 -0
- package/dist/index.d.cts +1501 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +1501 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index2.cjs +2767 -0
- package/dist/index2.cjs.map +1 -0
- package/dist/index2.js +2618 -0
- package/dist/index2.js.map +1 -0
- package/package.json +77 -0
- package/src/accordion/accordion.types.ts +126 -0
- package/src/accordion/header/AccordionHeader.vue +36 -0
- package/src/accordion/index.ts +10 -0
- package/src/accordion/item/AccordionItem.vue +124 -0
- package/src/accordion/item/AccordionItemContext.ts +24 -0
- package/src/accordion/item/AccordionItemDataAttributes.ts +15 -0
- package/src/accordion/item/stateAttributesMapping.ts +14 -0
- package/src/accordion/panel/AccordionPanel.vue +156 -0
- package/src/accordion/panel/AccordionPanelCssVars.ts +12 -0
- package/src/accordion/root/AccordionRoot.vue +130 -0
- package/src/accordion/root/AccordionRootContext.ts +37 -0
- package/src/accordion/root/AccordionRootDataAttributes.ts +10 -0
- package/src/accordion/root/stateAttributesMapping.ts +6 -0
- package/src/accordion/trigger/AccordionTrigger.vue +186 -0
- package/src/avatar/fallback/AvatarFallback.vue +75 -0
- package/src/avatar/image/AvatarImage.vue +103 -0
- package/src/avatar/image/AvatarImageDataAttributes.ts +14 -0
- package/src/avatar/image/useImageLoadingStatus.ts +58 -0
- package/src/avatar/index.ts +19 -0
- package/src/avatar/root/AvatarRoot.vue +62 -0
- package/src/avatar/root/AvatarRootContext.ts +22 -0
- package/src/avatar/root/stateAttributesMapping.ts +7 -0
- package/src/button/Button.vue +59 -0
- package/src/button/ButtonDataAttributes.ts +6 -0
- package/src/button/button.types.ts +22 -0
- package/src/button/index.ts +2 -0
- package/src/collapsible/collapsible.types.ts +64 -0
- package/src/collapsible/index.ts +6 -0
- package/src/collapsible/panel/CollapsiblePanel.vue +145 -0
- package/src/collapsible/panel/CollapsiblePanelCssVars.ts +12 -0
- package/src/collapsible/panel/CollapsiblePanelDataAttributes.ts +18 -0
- package/src/collapsible/panel/useCollapsiblePanel.ts +489 -0
- package/src/collapsible/root/CollapsibleRoot.vue +60 -0
- package/src/collapsible/root/CollapsibleRootContext.ts +18 -0
- package/src/collapsible/root/stateAttributesMapping.ts +9 -0
- package/src/collapsible/root/useCollapsibleRoot.ts +252 -0
- package/src/collapsible/trigger/CollapsibleTrigger.vue +63 -0
- package/src/collapsible/trigger/CollapsibleTriggerDataAttributes.ts +6 -0
- package/src/composite/composite.ts +232 -0
- package/src/composite/constants.ts +1 -0
- package/src/composite/item/CompositeItem.vue +75 -0
- package/src/composite/item/useCompositeItem.ts +63 -0
- package/src/composite/list/CompositeList.vue +168 -0
- package/src/composite/list/CompositeListContext.ts +21 -0
- package/src/composite/list/useCompositeListItem.ts +130 -0
- package/src/composite/root/CompositeRoot.vue +106 -0
- package/src/composite/root/CompositeRootContext.ts +36 -0
- package/src/composite/root/index.ts +7 -0
- package/src/composite/root/useCompositeRoot.ts +418 -0
- package/src/direction-provider/DirectionContext.ts +29 -0
- package/src/direction-provider/DirectionProvider.vue +31 -0
- package/src/direction-provider/index.ts +8 -0
- package/src/field/control/FieldControl.vue +211 -0
- package/src/field/control/FieldControlDataAttributes.ts +30 -0
- package/src/field/description/FieldDescription.vue +62 -0
- package/src/field/description/FieldDescriptionDataAttributes.ts +30 -0
- package/src/field/error/FieldError.vue +159 -0
- package/src/field/error/FieldErrorDataAttributes.ts +38 -0
- package/src/field/index.ts +27 -0
- package/src/field/item/FieldItem.vue +63 -0
- package/src/field/item/FieldItemContext.ts +16 -0
- package/src/field/label/FieldLabel.vue +102 -0
- package/src/field/label/FieldLabelDataAttributes.ts +30 -0
- package/src/field/root/FieldRoot.vue +262 -0
- package/src/field/root/FieldRootContext.ts +97 -0
- package/src/field/root/FieldRootDataAttributes.ts +30 -0
- package/src/field/root/useFieldRootState.ts +81 -0
- package/src/field/root/useFieldValidation.ts +298 -0
- package/src/field/root/useFieldValidity.ts +30 -0
- package/src/field/useField.ts +73 -0
- package/src/field/utils/constants.ts +45 -0
- package/src/field/utils/getCombinedFieldValidityData.ts +18 -0
- package/src/field/validity/FieldValidity.vue +36 -0
- package/src/fieldset/index.ts +8 -0
- package/src/fieldset/legend/FieldsetLegend.vue +72 -0
- package/src/fieldset/root/FieldsetRoot.vue +74 -0
- package/src/fieldset/root/FieldsetRootContext.ts +26 -0
- package/src/floating-ui-vue/types.ts +4 -0
- package/src/floating-ui-vue/utils/composite.ts +475 -0
- package/src/floating-ui-vue/utils/constants.ts +4 -0
- package/src/floating-ui-vue/utils/event.ts +4 -0
- package/src/floating-ui-vue/utils.ts +2 -0
- package/src/form/Form.vue +188 -0
- package/src/form/FormContext.ts +59 -0
- package/src/form/index.ts +10 -0
- package/src/index.ts +14 -0
- package/src/labelable-provider/LabelableContext.ts +33 -0
- package/src/labelable-provider/LabelableProvider.vue +55 -0
- package/src/labelable-provider/index.ts +6 -0
- package/src/labelable-provider/useAriaLabelledBy.ts +100 -0
- package/src/labelable-provider/useLabelableId.ts +30 -0
- package/src/merge-props/index.ts +1 -0
- package/src/merge-props/mergeProps.ts +192 -0
- package/src/test/index.ts +1 -0
- package/src/test/utils.ts +9 -0
- package/src/types/index.ts +10 -0
- package/src/use-button/index.ts +1 -0
- package/src/use-button/useButton.ts +231 -0
- package/src/use-render/index.ts +1 -0
- package/src/use-render/useRender.spec.ts +90 -0
- package/src/use-render/useRender.ts +152 -0
- package/src/utils/collapsibleOpenStateMapping.ts +33 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/createBaseUIEventDetails.ts +127 -0
- package/src/utils/empty.ts +5 -0
- package/src/utils/error.ts +19 -0
- package/src/utils/getStateAttributesProps.ts +31 -0
- package/src/utils/isElementDisabled.ts +7 -0
- package/src/utils/noop.ts +1 -0
- package/src/utils/reasons.ts +69 -0
- package/src/utils/resolveRef.ts +9 -0
- package/src/utils/slot.ts +6 -0
- package/src/utils/stateAttributesMapping.ts +28 -0
- package/src/utils/transitionStatusMapping.ts +22 -0
- package/src/utils/types.ts +47 -0
- package/src/utils/useAnimationFrame.ts +130 -0
- package/src/utils/useAnimationsFinished.ts +101 -0
- package/src/utils/useBaseUiId.ts +9 -0
- package/src/utils/useControllableState.ts +44 -0
- package/src/utils/useFocusableWhenDisabled.ts +85 -0
- package/src/utils/useId.ts +26 -0
- package/src/utils/useMergedRefs.ts +91 -0
- package/src/utils/useOpenChangeComplete.ts +52 -0
- package/src/utils/useRenderElement.ts +162 -0
- package/src/utils/useTimeout.ts +48 -0
- package/src/utils/useTransitionStatus.ts +104 -0
- 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,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>
|