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,252 @@
1
+ import type { Ref } from 'vue'
2
+ import type { TransitionStatus } from '../../utils/useTransitionStatus'
3
+ import type { CollapsibleChangeEventDetails, CollapsibleRootState } from '../collapsible.types'
4
+ import { computed, ref, shallowRef, watch } from 'vue'
5
+ import { createChangeEventDetails } from '../../utils/createBaseUIEventDetails'
6
+ import { REASONS } from '../../utils/reasons'
7
+ import { useAnimationsFinished } from '../../utils/useAnimationsFinished'
8
+ import { useBaseUiId } from '../../utils/useBaseUiId'
9
+ import { useControllableState } from '../../utils/useControllableState'
10
+ import { useTransitionStatus } from '../../utils/useTransitionStatus'
11
+
12
+ export type AnimationType = 'css-transition' | 'css-animation' | 'none' | null
13
+
14
+ export interface Dimensions {
15
+ height: number | undefined
16
+ width: number | undefined
17
+ }
18
+
19
+ export interface UseCollapsibleRootReturnValue {
20
+ abortControllerRef: Ref<AbortController | null>
21
+ animationTypeRef: Ref<AnimationType>
22
+ disabled: Ref<boolean>
23
+ handleTrigger: (event: MouseEvent | KeyboardEvent) => void
24
+ /**
25
+ * The height of the panel.
26
+ */
27
+ height: Ref<number | undefined>
28
+ /**
29
+ * Whether the collapsible panel is currently mounted.
30
+ */
31
+ mounted: Ref<boolean>
32
+ /**
33
+ * Whether the collapsible panel is currently open.
34
+ */
35
+ open: Ref<boolean>
36
+ panelId: Ref<string | undefined>
37
+ panelRef: Ref<HTMLElement | null>
38
+ runOnceAnimationsFinish: (
39
+ fn: () => void,
40
+ signal?: AbortSignal | null,
41
+ ) => void
42
+ setDimensions: (dims: Dimensions) => void
43
+ setHiddenUntilFound: (next: boolean) => void
44
+ setKeepMounted: (next: boolean) => void
45
+ setMounted: (next: boolean) => void
46
+ setOpen: (next: boolean) => void
47
+ setPanelIdState: (id: string | undefined) => void
48
+ setVisible: (next: boolean) => void
49
+ transitionDimensionRef: Ref<'width' | 'height' | null>
50
+ transitionStatus: Ref<TransitionStatus>
51
+ /**
52
+ * The visible state of the panel used to determine the `[hidden]` attribute
53
+ * only when CSS keyframe animations are used.
54
+ */
55
+ visible: Ref<boolean>
56
+ /**
57
+ * The width of the panel.
58
+ */
59
+ width: Ref<number | undefined>
60
+
61
+ keepMounted: Ref<boolean>
62
+ hiddenUntilFound: Ref<boolean>
63
+ state: Ref<CollapsibleRootState>
64
+ onOpenChange: (open: boolean, details: CollapsibleChangeEventDetails) => void
65
+ }
66
+
67
+ export interface UseCollapsibleRootParameters {
68
+ /**
69
+ * Whether the collapsible panel is currently open.
70
+ *
71
+ * To render an uncontrolled collapsible, use the `defaultOpen` prop instead.
72
+ */
73
+ open?: () => boolean | undefined
74
+ isOpenControlled?: () => boolean
75
+ /**
76
+ * Whether the collapsible panel is initially open.
77
+ *
78
+ * To render a controlled collapsible, use the `open` prop instead.
79
+ * @default false
80
+ */
81
+ defaultOpen?: boolean
82
+ /**
83
+ * Event handler called when the panel is opened or closed.
84
+ */
85
+ onOpenChange: (open: boolean, details: CollapsibleChangeEventDetails) => void
86
+ /**
87
+ * Whether the component should ignore user interaction.
88
+ * @default false
89
+ */
90
+ disabled: () => boolean
91
+ }
92
+
93
+ export function useCollapsibleRoot(
94
+ parameters: UseCollapsibleRootParameters,
95
+ ): UseCollapsibleRootReturnValue {
96
+ const {
97
+ open: openParam,
98
+ defaultOpen = false,
99
+ onOpenChange,
100
+ disabled: disabledGetter,
101
+ } = parameters
102
+
103
+ const isControlled = computed(() => {
104
+ if (parameters.isOpenControlled) {
105
+ return parameters.isOpenControlled()
106
+ }
107
+ return openParam?.() !== undefined
108
+ })
109
+
110
+ const { value: open, setValue: setOpen } = useControllableState<boolean>({
111
+ controlled: () => (isControlled.value ? openParam?.() : undefined),
112
+ default: defaultOpen,
113
+ })
114
+
115
+ const { mounted, setMounted, transitionStatus } = useTransitionStatus(
116
+ open as Ref<boolean>,
117
+ true,
118
+ true,
119
+ )
120
+ const visible = ref(open.value)
121
+ const height = ref<number | undefined>(undefined)
122
+ const width = ref<number | undefined>(undefined)
123
+
124
+ const defaultPanelId = useBaseUiId()
125
+ const panelIdState = ref<string | undefined>(undefined)
126
+ const panelId = computed(() => panelIdState.value ?? defaultPanelId)
127
+
128
+ const hiddenUntilFound = ref(false)
129
+ const keepMounted = ref(false)
130
+
131
+ const abortControllerRef = shallowRef<AbortController | null>(null)
132
+ const animationTypeRef = ref<AnimationType>(null)
133
+ const transitionDimensionRef = ref<'width' | 'height' | null>(null)
134
+ const panelRef = ref<HTMLElement | null>(null)
135
+
136
+ const runOnceAnimationsFinish = useAnimationsFinished(panelRef, false)
137
+
138
+ const disabled = computed(() => disabledGetter())
139
+
140
+ function handleTrigger(event: MouseEvent | KeyboardEvent) {
141
+ if (disabled.value) {
142
+ return
143
+ }
144
+
145
+ const nextOpen = !open.value
146
+ const eventDetails = createChangeEventDetails(REASONS.triggerPress, event)
147
+
148
+ onOpenChange(nextOpen, eventDetails)
149
+
150
+ if (eventDetails.isCanceled) {
151
+ return
152
+ }
153
+
154
+ const panel = panelRef.value
155
+
156
+ if (nextOpen && !mounted.value) {
157
+ // Ensure first open always mounts panel immediately.
158
+ setMounted(true)
159
+ }
160
+
161
+ if (animationTypeRef.value === 'css-animation' && panel != null) {
162
+ panel.style.removeProperty('animation-name')
163
+ }
164
+
165
+ if (!hiddenUntilFound.value && !keepMounted.value) {
166
+ if (
167
+ animationTypeRef.value != null
168
+ && animationTypeRef.value !== 'css-animation'
169
+ ) {
170
+ if (!mounted.value && nextOpen) {
171
+ setMounted(true)
172
+ }
173
+ }
174
+
175
+ if (animationTypeRef.value === 'css-animation') {
176
+ if (!visible.value && nextOpen) {
177
+ visible.value = true
178
+ }
179
+ if (!mounted.value && nextOpen) {
180
+ setMounted(true)
181
+ }
182
+ }
183
+ }
184
+
185
+ setOpen(nextOpen)
186
+
187
+ if (animationTypeRef.value === 'none' && mounted.value && !nextOpen) {
188
+ setMounted(false)
189
+ }
190
+ }
191
+
192
+ // Unmount immediately for controlled mode with no animations
193
+ watch(
194
+ open,
195
+ (isOpen) => {
196
+ if (
197
+ isControlled.value
198
+ && animationTypeRef.value === 'none'
199
+ && !keepMounted.value
200
+ && !isOpen
201
+ ) {
202
+ setMounted(false)
203
+ }
204
+ },
205
+ { flush: 'sync' },
206
+ )
207
+
208
+ const state = computed<CollapsibleRootState>(() => ({
209
+ open: open.value,
210
+ disabled: disabled.value,
211
+ transitionStatus: transitionStatus.value,
212
+ }))
213
+
214
+ return {
215
+ open: open as Ref<boolean>,
216
+ disabled,
217
+ panelId,
218
+ state,
219
+ handleTrigger,
220
+ mounted,
221
+ setMounted,
222
+ transitionStatus,
223
+ height,
224
+ width,
225
+ setDimensions(dims: Dimensions) {
226
+ height.value = dims.height
227
+ width.value = dims.width
228
+ },
229
+ setOpen,
230
+ visible,
231
+ setVisible(next: boolean) {
232
+ visible.value = next
233
+ },
234
+ keepMounted,
235
+ setKeepMounted(next: boolean) {
236
+ keepMounted.value = next
237
+ },
238
+ hiddenUntilFound,
239
+ setHiddenUntilFound(next: boolean) {
240
+ hiddenUntilFound.value = next
241
+ },
242
+ setPanelIdState(id: string | undefined) {
243
+ panelIdState.value = id
244
+ },
245
+ animationTypeRef,
246
+ panelRef,
247
+ abortControllerRef,
248
+ transitionDimensionRef,
249
+ runOnceAnimationsFinish,
250
+ onOpenChange,
251
+ }
252
+ }
@@ -0,0 +1,63 @@
1
+ <script setup lang="ts">
2
+ import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
3
+ import type { CollapsibleRootState, CollapsibleTriggerProps } from '../collapsible.types'
4
+ import { computed, useAttrs } from 'vue'
5
+ import { useButton } from '../../use-button'
6
+ import { triggerOpenStateMapping } from '../../utils/collapsibleOpenStateMapping'
7
+ import { transitionStatusMapping } from '../../utils/stateAttributesMapping'
8
+ import { useRenderElement } from '../../utils/useRenderElement'
9
+ import { useCollapsibleRootContext } from '../root/CollapsibleRootContext'
10
+
11
+ defineOptions({
12
+ name: 'CollapsibleTrigger',
13
+ inheritAttrs: false,
14
+ })
15
+
16
+ const props = withDefaults(defineProps<CollapsibleTriggerProps>(), {
17
+ as: 'button',
18
+ nativeButton: true,
19
+ })
20
+
21
+ const triggerStateAttributesMapping: StateAttributesMapping<CollapsibleRootState> = {
22
+ ...triggerOpenStateMapping,
23
+ ...transitionStatusMapping,
24
+ }
25
+
26
+ const attrs = useAttrs()
27
+
28
+ const ctx = useCollapsibleRootContext()
29
+
30
+ const disabled = computed(() => props.disabled ?? ctx.disabled.value)
31
+
32
+ const { getButtonProps, buttonRef } = useButton({
33
+ disabled: () => disabled.value,
34
+ focusableWhenDisabled: () => true,
35
+ native: () => props.nativeButton ?? true,
36
+ })
37
+
38
+ const {
39
+ tag,
40
+ mergedProps,
41
+ renderless,
42
+ ref: renderRef,
43
+ } = useRenderElement({
44
+ componentProps: props,
45
+ state: ctx.state,
46
+ props: computed(() => getButtonProps({
47
+ ...attrs as Record<string, any>,
48
+ 'aria-controls': ctx.open.value ? ctx.panelId.value : undefined,
49
+ 'aria-expanded': ctx.open.value,
50
+ 'onClick': ctx.handleTrigger,
51
+ })),
52
+ stateAttributesMapping: triggerStateAttributesMapping,
53
+ defaultTagName: 'button',
54
+ ref: buttonRef,
55
+ })
56
+ </script>
57
+
58
+ <template>
59
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="ctx.state" />
60
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
61
+ <slot :state="ctx.state" />
62
+ </component>
63
+ </template>
@@ -0,0 +1,6 @@
1
+ export enum CollapsibleTriggerDataAttributes {
2
+ /**
3
+ * Present when the collapsible panel is open.
4
+ */
5
+ panelOpen = 'data-panel-open',
6
+ }
@@ -0,0 +1,232 @@
1
+ import type { TextDirection } from '../direction-provider/DirectionContext'
2
+ import { isHTMLElement } from '@floating-ui/utils/dom'
3
+
4
+ export {
5
+ createGridCellMap,
6
+ findNonDisabledListIndex,
7
+ getGridCellIndexOfCorner,
8
+ getGridCellIndices,
9
+ getGridNavigatedIndex,
10
+ getMaxListIndex,
11
+ getMinListIndex,
12
+ isIndexOutOfListBounds,
13
+ isListIndexDisabled,
14
+ stopEvent,
15
+ } from '../floating-ui-vue/utils'
16
+
17
+ export interface Dimensions {
18
+ width: number
19
+ height: number
20
+ }
21
+
22
+ export const ARROW_UP = 'ArrowUp'
23
+ export const ARROW_DOWN = 'ArrowDown'
24
+ export const ARROW_LEFT = 'ArrowLeft'
25
+ export const ARROW_RIGHT = 'ArrowRight'
26
+ export const HOME = 'Home'
27
+ export const END = 'End'
28
+
29
+ export const HORIZONTAL_KEYS = new Set([ARROW_LEFT, ARROW_RIGHT])
30
+ export const HORIZONTAL_KEYS_WITH_EXTRA_KEYS = new Set([
31
+ ARROW_LEFT,
32
+ ARROW_RIGHT,
33
+ HOME,
34
+ END,
35
+ ])
36
+ export const VERTICAL_KEYS = new Set([ARROW_UP, ARROW_DOWN])
37
+ export const VERTICAL_KEYS_WITH_EXTRA_KEYS = new Set([
38
+ ARROW_UP,
39
+ ARROW_DOWN,
40
+ HOME,
41
+ END,
42
+ ])
43
+ export const ARROW_KEYS = new Set([...HORIZONTAL_KEYS, ...VERTICAL_KEYS])
44
+ export const ALL_KEYS = new Set([...ARROW_KEYS, HOME, END])
45
+ export const COMPOSITE_KEYS = new Set([
46
+ ARROW_UP,
47
+ ARROW_DOWN,
48
+ ARROW_LEFT,
49
+ ARROW_RIGHT,
50
+ HOME,
51
+ END,
52
+ ])
53
+
54
+ export const SHIFT = 'Shift' as const
55
+ export const CONTROL = 'Control' as const
56
+ export const ALT = 'Alt' as const
57
+ export const META = 'Meta' as const
58
+ export const MODIFIER_KEYS = new Set([SHIFT, CONTROL, ALT, META] as const)
59
+ export type ModifierKey
60
+ = typeof MODIFIER_KEYS extends Set<infer Keys> ? Keys : never
61
+
62
+ function isInputElement(element: EventTarget): element is HTMLInputElement {
63
+ return isHTMLElement(element) && element.tagName === 'INPUT'
64
+ }
65
+
66
+ export function isNativeInput(
67
+ element: EventTarget,
68
+ ): element is HTMLElement & (HTMLInputElement | HTMLTextAreaElement) {
69
+ if (isInputElement(element) && element.selectionStart != null) {
70
+ return true
71
+ }
72
+ if (isHTMLElement(element) && element.tagName === 'TEXTAREA') {
73
+ return true
74
+ }
75
+ return false
76
+ }
77
+
78
+ export function scrollIntoViewIfNeeded(
79
+ scrollContainer: HTMLElement | null,
80
+ element: HTMLElement | null,
81
+ direction: TextDirection,
82
+ orientation: 'horizontal' | 'vertical' | 'both',
83
+ ) {
84
+ if (!scrollContainer || !element || !element.scrollTo) {
85
+ return
86
+ }
87
+
88
+ let targetX = scrollContainer.scrollLeft
89
+ let targetY = scrollContainer.scrollTop
90
+
91
+ const isOverflowingX
92
+ = scrollContainer.clientWidth < scrollContainer.scrollWidth
93
+ const isOverflowingY
94
+ = scrollContainer.clientHeight < scrollContainer.scrollHeight
95
+
96
+ if (isOverflowingX && orientation !== 'vertical') {
97
+ const elementOffsetLeft = getOffset(scrollContainer, element, 'left')
98
+ const containerStyles = getStyles(scrollContainer)
99
+ const elementStyles = getStyles(element)
100
+
101
+ if (direction === 'ltr') {
102
+ if (
103
+ elementOffsetLeft
104
+ + element.offsetWidth
105
+ + elementStyles.scrollMarginRight
106
+ > scrollContainer.scrollLeft
107
+ + scrollContainer.clientWidth
108
+ - containerStyles.scrollPaddingRight
109
+ ) {
110
+ // overflow to the right, scroll to align right edges
111
+ targetX
112
+ = elementOffsetLeft
113
+ + element.offsetWidth
114
+ + elementStyles.scrollMarginRight
115
+ - scrollContainer.clientWidth
116
+ + containerStyles.scrollPaddingRight
117
+ }
118
+ else if (
119
+ elementOffsetLeft - elementStyles.scrollMarginLeft
120
+ < scrollContainer.scrollLeft + containerStyles.scrollPaddingLeft
121
+ ) {
122
+ // overflow to the left, scroll to align left edges
123
+ targetX
124
+ = elementOffsetLeft
125
+ - elementStyles.scrollMarginLeft
126
+ - containerStyles.scrollPaddingLeft
127
+ }
128
+ }
129
+
130
+ if (direction === 'rtl') {
131
+ if (
132
+ elementOffsetLeft - elementStyles.scrollMarginRight
133
+ < scrollContainer.scrollLeft + containerStyles.scrollPaddingLeft
134
+ ) {
135
+ // overflow to the left, scroll to align left edges
136
+ targetX
137
+ = elementOffsetLeft
138
+ - elementStyles.scrollMarginLeft
139
+ - containerStyles.scrollPaddingLeft
140
+ }
141
+ else if (
142
+ elementOffsetLeft
143
+ + element.offsetWidth
144
+ + elementStyles.scrollMarginRight
145
+ > scrollContainer.scrollLeft
146
+ + scrollContainer.clientWidth
147
+ - containerStyles.scrollPaddingRight
148
+ ) {
149
+ // overflow to the right, scroll to align right edges
150
+ targetX
151
+ = elementOffsetLeft
152
+ + element.offsetWidth
153
+ + elementStyles.scrollMarginRight
154
+ - scrollContainer.clientWidth
155
+ + containerStyles.scrollPaddingRight
156
+ }
157
+ }
158
+ }
159
+
160
+ if (isOverflowingY && orientation !== 'horizontal') {
161
+ const elementOffsetTop = getOffset(scrollContainer, element, 'top')
162
+ const containerStyles = getStyles(scrollContainer)
163
+ const elementStyles = getStyles(element)
164
+
165
+ if (
166
+ elementOffsetTop - elementStyles.scrollMarginTop
167
+ < scrollContainer.scrollTop + containerStyles.scrollPaddingTop
168
+ ) {
169
+ // overflow upwards, align top edges
170
+ targetY
171
+ = elementOffsetTop
172
+ - elementStyles.scrollMarginTop
173
+ - containerStyles.scrollPaddingTop
174
+ }
175
+ else if (
176
+ elementOffsetTop
177
+ + element.offsetHeight
178
+ + elementStyles.scrollMarginBottom
179
+ > scrollContainer.scrollTop
180
+ + scrollContainer.clientHeight
181
+ - containerStyles.scrollPaddingBottom
182
+ ) {
183
+ // overflow downwards, align bottom edges
184
+ targetY
185
+ = elementOffsetTop
186
+ + element.offsetHeight
187
+ + elementStyles.scrollMarginBottom
188
+ - scrollContainer.clientHeight
189
+ + containerStyles.scrollPaddingBottom
190
+ }
191
+ }
192
+
193
+ scrollContainer.scrollTo({
194
+ left: targetX,
195
+ top: targetY,
196
+ behavior: 'auto',
197
+ })
198
+ }
199
+
200
+ function getOffset(
201
+ ancestor: HTMLElement,
202
+ element: HTMLElement,
203
+ side: 'left' | 'top',
204
+ ) {
205
+ const propName = side === 'left' ? 'offsetLeft' : 'offsetTop'
206
+
207
+ let result = 0
208
+
209
+ while (element.offsetParent) {
210
+ result += element[propName as keyof HTMLElement] as number
211
+ if (element.offsetParent === ancestor) {
212
+ break
213
+ }
214
+ element = element.offsetParent as HTMLElement
215
+ }
216
+
217
+ return result
218
+ }
219
+
220
+ function getStyles(element: HTMLElement) {
221
+ const styles = getComputedStyle(element)
222
+ return {
223
+ scrollMarginTop: Number.parseFloat(styles.scrollMarginTop) || 0,
224
+ scrollMarginRight: Number.parseFloat(styles.scrollMarginRight) || 0,
225
+ scrollMarginBottom: Number.parseFloat(styles.scrollMarginBottom) || 0,
226
+ scrollMarginLeft: Number.parseFloat(styles.scrollMarginLeft) || 0,
227
+ scrollPaddingTop: Number.parseFloat(styles.scrollPaddingTop) || 0,
228
+ scrollPaddingRight: Number.parseFloat(styles.scrollPaddingRight) || 0,
229
+ scrollPaddingBottom: Number.parseFloat(styles.scrollPaddingBottom) || 0,
230
+ scrollPaddingLeft: Number.parseFloat(styles.scrollPaddingLeft) || 0,
231
+ }
232
+ }
@@ -0,0 +1 @@
1
+ export const ACTIVE_COMPOSITE_ITEM = 'data-composite-item-active'
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts" generic="Metadata, State extends Record<string, any> = Record<string, any>">
2
+ import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
3
+ import type { BaseUIComponentProps } from '../../utils/types'
4
+ import { computed, useAttrs } from 'vue'
5
+ import { EMPTY_OBJECT } from '../../utils/constants'
6
+ import { useRenderElement } from '../../utils/useRenderElement'
7
+ import { useCompositeItem } from './useCompositeItem'
8
+
9
+ export interface CompositeItemProps<Metadata, State extends Record<string, any>> extends Pick<
10
+ BaseUIComponentProps<State>,
11
+ 'class'
12
+ > {
13
+ as?: string | any
14
+ style?: BaseUIComponentProps<State>['style']
15
+ metadata?: Metadata
16
+ refs?: Array<HTMLElement | null>
17
+ props?: Array<Record<string, any> | (() => Record<string, any>)>
18
+ stateAttributesMapping?: StateAttributesMapping<State>
19
+ state?: State
20
+ }
21
+
22
+ defineOptions({
23
+ name: 'BaseUICompositeItem',
24
+ inheritAttrs: false,
25
+ })
26
+
27
+ const props = withDefaults(defineProps<CompositeItemProps<Metadata, State>>(), {
28
+ as: 'div',
29
+ state: () => EMPTY_OBJECT as State,
30
+ })
31
+
32
+ const attrs = useAttrs()
33
+
34
+ const { compositeProps, compositeRef } = useCompositeItem({
35
+ metadata: () => props.metadata,
36
+ })
37
+
38
+ const externalProps = computed(() => {
39
+ let externalProps = {}
40
+
41
+ if (props.props) {
42
+ props.props.forEach((prop) => {
43
+ const p = typeof prop === 'function' ? prop() : prop
44
+ externalProps = { ...externalProps, ...p }
45
+ })
46
+ }
47
+
48
+ return {
49
+ ...attrs,
50
+ ...externalProps,
51
+ ...compositeProps.value,
52
+ }
53
+ })
54
+
55
+ const {
56
+ tag,
57
+ mergedProps,
58
+ renderless,
59
+ ref: renderRef,
60
+ } = useRenderElement({
61
+ componentProps: props,
62
+ state: computed(() => props.state),
63
+ props: externalProps,
64
+ stateAttributesMapping: props.stateAttributesMapping,
65
+ defaultTagName: 'div',
66
+ ref: compositeRef,
67
+ })
68
+ </script>
69
+
70
+ <template>
71
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="props.state" />
72
+ <component :is="tag" v-else :ref="renderRef" v-bind="mergedProps">
73
+ <slot :state="props.state" />
74
+ </component>
75
+ </template>
@@ -0,0 +1,63 @@
1
+ import type { HTMLProps } from '../../utils/types'
2
+ import type { UseCompositeListItemParameters } from '../list/useCompositeListItem'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { useMergedRefs } from '../../utils/useMergedRefs'
5
+ import { useCompositeListItem } from '../list/useCompositeListItem'
6
+ import { useCompositeRootContext } from '../root/CompositeRootContext'
7
+
8
+ export interface UseCompositeItemParameters<Metadata> extends Pick<
9
+ UseCompositeListItemParameters<Metadata>,
10
+ 'metadata' | 'indexGuessBehavior'
11
+ > {}
12
+
13
+ export function useCompositeItem<Metadata>(
14
+ params: UseCompositeItemParameters<Metadata> = {},
15
+ ) {
16
+ const context = useCompositeRootContext()
17
+
18
+ const { ref: listItemRef, index } = useCompositeListItem(params)
19
+
20
+ const isHighlighted = computed(
21
+ () => context.highlightedIndex === index.value,
22
+ )
23
+
24
+ const itemRef = ref<HTMLElement | null>(null)
25
+ const mergedRef = useMergedRefs(listItemRef, itemRef)
26
+
27
+ // When the index changes from -1 to a valid value, if this item
28
+ // currently has DOM focus, update the highlighted index.
29
+ // This handles the case where focus() is called before items are registered.
30
+ watch(index, (newIndex, oldIndex) => {
31
+ if (oldIndex === -1 && newIndex !== -1 && itemRef.value) {
32
+ if (document.activeElement === itemRef.value) {
33
+ context.onHighlightedIndexChange(newIndex)
34
+ }
35
+ }
36
+ })
37
+
38
+ const compositeProps = computed<HTMLProps>(() => ({
39
+ tabindex: isHighlighted.value ? 0 : -1,
40
+ onFocus() {
41
+ context.onHighlightedIndexChange(index.value)
42
+ },
43
+ onMousemove() {
44
+ const item = itemRef.value
45
+ if (!context.highlightItemOnHover || !item) {
46
+ return
47
+ }
48
+
49
+ const disabled
50
+ = item.hasAttribute('disabled')
51
+ || item.getAttribute('aria-disabled') === 'true'
52
+ if (!isHighlighted.value && !disabled) {
53
+ item.focus()
54
+ }
55
+ },
56
+ }))
57
+
58
+ return {
59
+ compositeProps,
60
+ compositeRef: mergedRef,
61
+ index,
62
+ }
63
+ }