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,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,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
|
+
}
|