base-ui-vue 0.1.0 → 0.3.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/README.md +41 -1
- package/dist/button/Button.cjs +53 -12
- package/dist/button/Button.cjs.map +1 -1
- package/dist/button/Button.js +26 -15
- package/dist/button/Button.js.map +1 -1
- package/dist/button/ToolbarButton.cjs +367 -0
- package/dist/button/ToolbarButton.cjs.map +1 -0
- package/dist/button/ToolbarButton.js +320 -0
- package/dist/button/ToolbarButton.js.map +1 -0
- package/dist/button/ToolbarButtonDataAttributes.cjs +27 -0
- package/dist/button/ToolbarButtonDataAttributes.cjs.map +1 -0
- package/dist/button/ToolbarButtonDataAttributes.js +21 -0
- package/dist/button/ToolbarButtonDataAttributes.js.map +1 -0
- package/dist/checkbox/index.cjs +1173 -0
- package/dist/checkbox/index.cjs.map +1 -0
- package/dist/checkbox/index.js +1048 -0
- package/dist/checkbox/index.js.map +1 -0
- package/dist/checkbox-group/CheckboxGroup.cjs +629 -0
- package/dist/checkbox-group/CheckboxGroup.cjs.map +1 -0
- package/dist/checkbox-group/CheckboxGroup.js +540 -0
- package/dist/checkbox-group/CheckboxGroup.js.map +1 -0
- package/dist/checkbox-group/CheckboxGroupDataAttributes.cjs +18 -0
- package/dist/checkbox-group/CheckboxGroupDataAttributes.cjs.map +1 -0
- package/dist/checkbox-group/CheckboxGroupDataAttributes.js +12 -0
- package/dist/checkbox-group/CheckboxGroupDataAttributes.js.map +1 -0
- package/dist/composite/composite.cjs +167 -0
- package/dist/composite/composite.cjs.map +1 -1
- package/dist/composite/composite.js +96 -1
- package/dist/composite/composite.js.map +1 -1
- package/dist/composite/constants.cjs +12 -0
- package/dist/composite/constants.cjs.map +1 -0
- package/dist/composite/constants.js +6 -0
- package/dist/composite/constants.js.map +1 -0
- package/dist/content/ScrollAreaContent.cjs +168 -0
- package/dist/content/ScrollAreaContent.cjs.map +1 -0
- package/dist/content/ScrollAreaContent.js +133 -0
- package/dist/content/ScrollAreaContent.js.map +1 -0
- package/dist/control/FieldControl.cjs +18 -343
- package/dist/control/FieldControl.cjs.map +1 -1
- package/dist/control/FieldControl.js +14 -285
- package/dist/control/FieldControl.js.map +1 -1
- package/dist/control/SliderControl.cjs +636 -0
- package/dist/control/SliderControl.cjs.map +1 -0
- package/dist/control/SliderControl.js +553 -0
- package/dist/control/SliderControl.js.map +1 -0
- package/dist/control/SliderControlDataAttributes.cjs +47 -0
- package/dist/control/SliderControlDataAttributes.cjs.map +1 -0
- package/dist/control/SliderControlDataAttributes.js +41 -0
- package/dist/control/SliderControlDataAttributes.js.map +1 -0
- package/dist/corner/ScrollAreaCorner.cjs +77 -0
- package/dist/corner/ScrollAreaCorner.cjs.map +1 -0
- package/dist/corner/ScrollAreaCorner.js +72 -0
- package/dist/corner/ScrollAreaCorner.js.map +1 -0
- package/dist/csp-provider/CSPContext.cjs +32 -0
- package/dist/csp-provider/CSPContext.cjs.map +1 -0
- package/dist/csp-provider/CSPContext.js +21 -0
- package/dist/csp-provider/CSPContext.js.map +1 -0
- package/dist/csp-provider/CSPProvider.cjs +46 -0
- package/dist/csp-provider/CSPProvider.cjs.map +1 -0
- package/dist/csp-provider/CSPProvider.js +41 -0
- package/dist/csp-provider/CSPProvider.js.map +1 -0
- package/dist/description/FieldDescription.cjs +5 -5
- package/dist/description/FieldDescription.cjs.map +1 -1
- package/dist/description/FieldDescription.js +1 -1
- package/dist/direction-provider/DirectionProvider.cjs +2 -2
- package/dist/direction-provider/DirectionProvider.cjs.map +1 -1
- package/dist/direction-provider/DirectionProvider.js +1 -1
- package/dist/error/FieldError.cjs +10 -288
- package/dist/error/FieldError.cjs.map +1 -1
- package/dist/error/FieldError.js +4 -246
- package/dist/error/FieldError.js.map +1 -1
- package/dist/form/Form.cjs +5 -4
- package/dist/form/Form.cjs.map +1 -1
- package/dist/form/Form.js +5 -4
- package/dist/form/Form.js.map +1 -1
- package/dist/group/ToolbarGroup.cjs +92 -0
- package/dist/group/ToolbarGroup.cjs.map +1 -0
- package/dist/group/ToolbarGroup.js +87 -0
- package/dist/group/ToolbarGroup.js.map +1 -0
- package/dist/group/ToolbarGroupDataAttributes.cjs +23 -0
- package/dist/group/ToolbarGroupDataAttributes.cjs.map +1 -0
- package/dist/group/ToolbarGroupDataAttributes.js +17 -0
- package/dist/group/ToolbarGroupDataAttributes.js.map +1 -0
- package/dist/header/AccordionHeader.cjs +2 -2
- package/dist/header/AccordionHeader.js +1 -1
- package/dist/image/AvatarImage.cjs +4 -4
- package/dist/image/AvatarImage.cjs.map +1 -1
- package/dist/image/AvatarImage.js +1 -1
- package/dist/index.cjs +113 -10
- package/dist/index.d.cts +3457 -603
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3457 -603
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -5
- package/dist/index2.cjs +5897 -424
- package/dist/index2.cjs.map +1 -1
- package/dist/index2.js +5389 -270
- package/dist/index2.js.map +1 -1
- package/package.json +8 -4
- package/src/accordion/root/AccordionRoot.vue +2 -1
- package/src/checkbox/index.ts +23 -0
- package/src/checkbox/indicator/CheckboxIndicator.vue +102 -0
- package/src/checkbox/indicator/CheckboxIndicatorDataAttributes.ts +61 -0
- package/src/checkbox/root/CheckboxRoot.vue +632 -0
- package/src/checkbox/root/CheckboxRootContext.ts +22 -0
- package/src/checkbox/root/CheckboxRootDataAttributes.ts +54 -0
- package/src/checkbox/utils/useStateAttributesMapping.ts +30 -0
- package/src/checkbox-group/CheckboxGroup.vue +241 -0
- package/src/checkbox-group/CheckboxGroupContext.ts +39 -0
- package/src/checkbox-group/CheckboxGroupDataAttributes.ts +6 -0
- package/src/checkbox-group/index.ts +11 -0
- package/src/checkbox-group/useCheckboxGroupParent.ts +173 -0
- package/src/collapsible/panel/useCollapsiblePanel.ts +2 -1
- package/src/collapsible/root/useCollapsibleRoot.ts +3 -1
- package/src/composite/composite.ts +2 -0
- package/src/composite/item/CompositeItem.vue +7 -8
- package/src/composite/root/CompositeRoot.vue +12 -1
- package/src/csp-provider/CSPContext.ts +26 -0
- package/src/csp-provider/CSPProvider.vue +40 -0
- package/src/csp-provider/index.ts +5 -0
- package/src/field/item/FieldItem.vue +6 -1
- package/src/field/label/FieldLabel.vue +10 -51
- package/src/field/root/FieldRoot.vue +16 -3
- package/src/floating-ui-vue/types.ts +1 -4
- package/src/floating-ui-vue/utils/element.ts +12 -0
- package/src/floating-ui-vue/utils/shadowDom.ts +44 -0
- package/src/floating-ui-vue/utils.ts +3 -0
- package/src/form/Form.vue +5 -3
- package/src/index.ts +13 -0
- package/src/input/Input.vue +37 -0
- package/src/input/InputDataAttributes.ts +30 -0
- package/src/input/index.ts +4 -0
- package/src/labelable-provider/LabelableContext.ts +2 -2
- package/src/labelable-provider/LabelableProvider.vue +21 -4
- package/src/labelable-provider/index.ts +2 -0
- package/src/labelable-provider/useAriaLabelledBy.ts +9 -9
- package/src/labelable-provider/useLabel.ts +115 -0
- package/src/labelable-provider/useLabelableId.ts +12 -10
- package/src/meter/index.ts +16 -0
- package/src/meter/indicator/MeterIndicator.vue +65 -0
- package/src/meter/label/MeterLabel.vue +63 -0
- package/src/meter/root/MeterRoot.vue +131 -0
- package/src/meter/root/MeterRootContext.ts +41 -0
- package/src/meter/track/MeterTrack.vue +46 -0
- package/src/meter/value/MeterValue.vue +85 -0
- package/src/progress/index.ts +23 -0
- package/src/progress/indicator/ProgressIndicator.vue +74 -0
- package/src/progress/label/ProgressLabel.vue +63 -0
- package/src/progress/root/ProgressRoot.vue +160 -0
- package/src/progress/root/ProgressRootContext.ts +51 -0
- package/src/progress/root/ProgressRootDataAttributes.ts +14 -0
- package/src/progress/root/stateAttributesMapping.ts +18 -0
- package/src/progress/track/ProgressTrack.vue +48 -0
- package/src/progress/value/ProgressValue.vue +92 -0
- package/src/scroll-area/constants.ts +2 -0
- package/src/scroll-area/content/ScrollAreaContent.vue +87 -0
- package/src/scroll-area/corner/ScrollAreaCorner.vue +64 -0
- package/src/scroll-area/index.ts +25 -0
- package/src/scroll-area/root/ScrollAreaRoot.vue +297 -0
- package/src/scroll-area/root/ScrollAreaRootContext.ts +89 -0
- package/src/scroll-area/root/ScrollAreaRootCssVars.ts +4 -0
- package/src/scroll-area/root/ScrollAreaRootDataAttributes.ts +9 -0
- package/src/scroll-area/root/stateAttributes.ts +14 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbar.vue +263 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarContext.ts +20 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarCssVars.ts +4 -0
- package/src/scroll-area/scrollbar/ScrollAreaScrollbarDataAttributes.ts +11 -0
- package/src/scroll-area/thumb/ScrollAreaThumb.vue +120 -0
- package/src/scroll-area/thumb/ScrollAreaThumbDataAttributes.ts +3 -0
- package/src/scroll-area/utils/getOffset.ts +34 -0
- package/src/scroll-area/viewport/ScrollAreaViewport.vue +379 -0
- package/src/scroll-area/viewport/ScrollAreaViewportContext.ts +20 -0
- package/src/scroll-area/viewport/ScrollAreaViewportCssVars.ts +6 -0
- package/src/scroll-area/viewport/ScrollAreaViewportDataAttributes.ts +9 -0
- package/src/separator/Separator.vue +65 -0
- package/src/separator/SeparatorDataAttributes.ts +7 -0
- package/src/separator/index.ts +3 -0
- package/src/slider/control/SliderControl.vue +497 -0
- package/src/slider/control/SliderControlDataAttributes.ts +35 -0
- package/src/slider/index.ts +35 -0
- package/src/slider/indicator/SliderIndicator.vue +144 -0
- package/src/slider/indicator/SliderIndicatorDataAttributes.ts +35 -0
- package/src/slider/label/SliderLabel.vue +75 -0
- package/src/slider/root/SliderRoot.vue +557 -0
- package/src/slider/root/SliderRootContext.ts +126 -0
- package/src/slider/root/SliderRootDataAttributes.ts +35 -0
- package/src/slider/root/stateAttributesMapping.ts +13 -0
- package/src/slider/thumb/SliderThumb.vue +601 -0
- package/src/slider/thumb/SliderThumbDataAttributes.ts +39 -0
- package/src/slider/thumb/prehydrationScript.min.ts +5 -0
- package/src/slider/thumb/prehydrationScript.template.js +69 -0
- package/src/slider/track/SliderTrack.vue +48 -0
- package/src/slider/track/SliderTrackDataAttributes.ts +10 -0
- package/src/slider/utils/asc.ts +3 -0
- package/src/slider/utils/getMidpoint.ts +9 -0
- package/src/slider/utils/getPushedThumbValues.ts +68 -0
- package/src/slider/utils/getSliderValue.ts +25 -0
- package/src/slider/utils/replaceArrayItemAtIndex.ts +15 -0
- package/src/slider/utils/resolveThumbCollision.ts +177 -0
- package/src/slider/utils/roundValueToStep.ts +19 -0
- package/src/slider/utils/test-utils.ts +25 -0
- package/src/slider/utils/validateMinimumDistance.ts +20 -0
- package/src/slider/utils/valueArrayToPercentages.ts +10 -0
- package/src/slider/value/SliderValue.vue +90 -0
- package/src/slider/value/SliderValueDataAttributes.ts +35 -0
- package/src/switch/index.ts +14 -0
- package/src/switch/root/SwitchRoot.vue +448 -0
- package/src/switch/root/SwitchRootContext.ts +22 -0
- package/src/switch/root/SwitchRootDataAttributes.ts +46 -0
- package/src/switch/stateAttributesMapping.ts +23 -0
- package/src/switch/thumb/SwitchThumb.vue +59 -0
- package/src/switch/thumb/SwitchThumbDataAttributes.ts +46 -0
- package/src/toggle/Toggle.vue +211 -0
- package/src/toggle/ToggleDataAttributes.ts +6 -0
- package/src/toggle/index.ts +3 -0
- package/src/toggle-group/ToggleGroup.vue +224 -0
- package/src/toggle-group/ToggleGroupContext.ts +45 -0
- package/src/toggle-group/ToggleGroupDataAttributes.ts +15 -0
- package/src/toggle-group/index.ts +5 -0
- package/src/toolbar/button/ToolbarButton.vue +99 -0
- package/src/toolbar/button/ToolbarButtonDataAttributes.ts +15 -0
- package/src/toolbar/group/ToolbarGroup.vue +70 -0
- package/src/toolbar/group/ToolbarGroupContext.ts +23 -0
- package/src/toolbar/group/ToolbarGroupDataAttributes.ts +11 -0
- package/src/toolbar/index.ts +27 -0
- package/src/toolbar/input/ToolbarInput.vue +114 -0
- package/src/toolbar/input/ToolbarInputDataAttributes.ts +15 -0
- package/src/toolbar/link/ToolbarLink.vue +54 -0
- package/src/toolbar/link/ToolbarLinkDataAttributes.ts +7 -0
- package/src/toolbar/root/ToolbarRoot.vue +144 -0
- package/src/toolbar/root/ToolbarRootContext.ts +29 -0
- package/src/toolbar/root/ToolbarRootDataAttributes.ts +11 -0
- package/src/toolbar/separator/ToolbarSeparator.vue +41 -0
- package/src/toolbar/separator/ToolbarSeparatorDataAttributes.ts +7 -0
- package/src/use-button/useButton.ts +2 -1
- package/src/utils/areArraysEqual.ts +12 -0
- package/src/utils/clamp.ts +7 -0
- package/src/utils/createBaseUIEventDetails.ts +9 -0
- package/src/utils/formatNumber.ts +32 -0
- package/src/utils/owner.ts +5 -0
- package/src/utils/resolveAriaLabelledBy.ts +10 -0
- package/src/utils/scrollEdges.ts +33 -0
- package/src/utils/styles.ts +28 -0
- package/src/utils/useControllableState.ts +78 -14
- package/src/utils/useFocusableWhenDisabled.ts +6 -1
- package/src/utils/useMergedRefs.ts +26 -2
- package/src/utils/useRegisteredLabelId.ts +21 -0
- package/src/utils/valueToPercent.ts +7 -0
- package/src/utils/visuallyHidden.ts +24 -0
- package/dist/direction-provider/DirectionContext.cjs +0 -26
- package/dist/direction-provider/DirectionContext.cjs.map +0 -1
- package/dist/direction-provider/DirectionContext.js +0 -15
- package/dist/direction-provider/DirectionContext.js.map +0 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
3
|
+
import type { Coords, HiddenState, OverflowEdges, ScrollAreaRootContext, ScrollAreaRootState, Size } from './ScrollAreaRootContext'
|
|
4
|
+
import { computed, onMounted, provide, ref, shallowRef, useAttrs } from 'vue'
|
|
5
|
+
import { useCSPContext } from '../../csp-provider/CSPContext'
|
|
6
|
+
import { contains } from '../../floating-ui-vue/utils/shadowDom'
|
|
7
|
+
import { mergeProps } from '../../merge-props/mergeProps'
|
|
8
|
+
import { styleDisableScrollbar } from '../../utils/styles'
|
|
9
|
+
import { useBaseUiId } from '../../utils/useBaseUiId'
|
|
10
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
11
|
+
import { SCROLL_TIMEOUT } from '../constants'
|
|
12
|
+
import { ScrollAreaScrollbarDataAttributes } from '../scrollbar/ScrollAreaScrollbarDataAttributes'
|
|
13
|
+
import { getOffset } from '../utils/getOffset'
|
|
14
|
+
import { scrollAreaRootContextKey } from './ScrollAreaRootContext'
|
|
15
|
+
import { ScrollAreaRootCssVars } from './ScrollAreaRootCssVars'
|
|
16
|
+
import { scrollAreaStateAttributesMapping } from './stateAttributes'
|
|
17
|
+
|
|
18
|
+
defineOptions({
|
|
19
|
+
name: 'ScrollAreaRoot',
|
|
20
|
+
inheritAttrs: false,
|
|
21
|
+
})
|
|
22
|
+
const props = withDefaults(defineProps<ScrollAreaRootProps>(), {
|
|
23
|
+
as: 'div',
|
|
24
|
+
})
|
|
25
|
+
const DEFAULT_SIZE: Size = { width: 0, height: 0 }
|
|
26
|
+
const DEFAULT_OVERFLOW_EDGES: OverflowEdges = { xStart: false, xEnd: false, yStart: false, yEnd: false }
|
|
27
|
+
const DEFAULT_HIDDEN_STATE: HiddenState = { x: true, y: true, corner: true }
|
|
28
|
+
const DEFAULT_COORDS: Coords = { x: 0, y: 0 }
|
|
29
|
+
|
|
30
|
+
export interface ScrollAreaRootProps extends BaseUIComponentProps<ScrollAreaRootState> {
|
|
31
|
+
overflowEdgeThreshold?:
|
|
32
|
+
| number
|
|
33
|
+
| Partial<{
|
|
34
|
+
xStart: number
|
|
35
|
+
xEnd: number
|
|
36
|
+
yStart: number
|
|
37
|
+
yEnd: number
|
|
38
|
+
}>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const attrs = useAttrs()
|
|
42
|
+
|
|
43
|
+
const overflowEdgeThreshold = computed(() => normalizeOverflowEdgeThreshold(props.overflowEdgeThreshold))
|
|
44
|
+
|
|
45
|
+
const rootId = useBaseUiId()
|
|
46
|
+
const { nonce, disableStyleElements } = useCSPContext()
|
|
47
|
+
|
|
48
|
+
const hovering = ref(false)
|
|
49
|
+
const scrollingX = ref(false)
|
|
50
|
+
const scrollingY = ref(false)
|
|
51
|
+
const touchModality = ref(false)
|
|
52
|
+
const hasMeasuredScrollbar = ref(false)
|
|
53
|
+
const cornerSize = ref<Size>({ ...DEFAULT_SIZE })
|
|
54
|
+
const thumbSize = ref<Size>({ ...DEFAULT_SIZE })
|
|
55
|
+
const overflowEdges = ref<OverflowEdges>({ ...DEFAULT_OVERFLOW_EDGES })
|
|
56
|
+
const hiddenState = ref<HiddenState>({ ...DEFAULT_HIDDEN_STATE })
|
|
57
|
+
|
|
58
|
+
const rootRef = shallowRef<HTMLDivElement | null>(null)
|
|
59
|
+
const viewportRef = shallowRef<HTMLDivElement | null>(null)
|
|
60
|
+
const scrollbarYRef = shallowRef<HTMLDivElement | null>(null)
|
|
61
|
+
const scrollbarXRef = shallowRef<HTMLDivElement | null>(null)
|
|
62
|
+
const thumbYRef = shallowRef<HTMLDivElement | null>(null)
|
|
63
|
+
const thumbXRef = shallowRef<HTMLDivElement | null>(null)
|
|
64
|
+
const cornerRef = shallowRef<HTMLDivElement | null>(null)
|
|
65
|
+
|
|
66
|
+
let scrollYTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
67
|
+
let scrollXTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
68
|
+
|
|
69
|
+
// Pointer drag baselines are mutable so move events don't trigger Vue renders.
|
|
70
|
+
const thumbDragging = { current: false }
|
|
71
|
+
const startY = { current: 0 }
|
|
72
|
+
const startX = { current: 0 }
|
|
73
|
+
const startScrollTop = { current: 0 }
|
|
74
|
+
const startScrollLeft = { current: 0 }
|
|
75
|
+
const currentOrientation = { current: 'vertical' as 'vertical' | 'horizontal' }
|
|
76
|
+
const scrollPosition = { current: { ...DEFAULT_COORDS } }
|
|
77
|
+
|
|
78
|
+
function handleScroll(pos: Coords) {
|
|
79
|
+
const offsetX = pos.x - scrollPosition.current.x
|
|
80
|
+
const offsetY = pos.y - scrollPosition.current.y
|
|
81
|
+
scrollPosition.current = pos
|
|
82
|
+
|
|
83
|
+
if (offsetY !== 0) {
|
|
84
|
+
scrollingY.value = true
|
|
85
|
+
clearTimeout(scrollYTimeoutId)
|
|
86
|
+
scrollYTimeoutId = setTimeout(() => {
|
|
87
|
+
scrollingY.value = false
|
|
88
|
+
}, SCROLL_TIMEOUT)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (offsetX !== 0) {
|
|
92
|
+
scrollingX.value = true
|
|
93
|
+
clearTimeout(scrollXTimeoutId)
|
|
94
|
+
scrollXTimeoutId = setTimeout(() => {
|
|
95
|
+
scrollingX.value = false
|
|
96
|
+
}, SCROLL_TIMEOUT)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handlePointerDown(event: PointerEvent) {
|
|
101
|
+
if (event.button !== 0)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
thumbDragging.current = true
|
|
105
|
+
startY.current = event.clientY
|
|
106
|
+
startX.current = event.clientX
|
|
107
|
+
currentOrientation.current = (event.currentTarget as Element).getAttribute(
|
|
108
|
+
ScrollAreaScrollbarDataAttributes.orientation,
|
|
109
|
+
) as 'vertical' | 'horizontal'
|
|
110
|
+
|
|
111
|
+
if (viewportRef.value) {
|
|
112
|
+
startScrollTop.current = viewportRef.value.scrollTop
|
|
113
|
+
startScrollLeft.current = viewportRef.value.scrollLeft
|
|
114
|
+
}
|
|
115
|
+
if (thumbYRef.value && currentOrientation.current === 'vertical') {
|
|
116
|
+
thumbYRef.value.setPointerCapture(event.pointerId)
|
|
117
|
+
}
|
|
118
|
+
if (thumbXRef.value && currentOrientation.current === 'horizontal') {
|
|
119
|
+
thumbXRef.value.setPointerCapture(event.pointerId)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function handlePointerMove(event: PointerEvent) {
|
|
124
|
+
if (!thumbDragging.current)
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
const deltaY = event.clientY - startY.current
|
|
128
|
+
const deltaX = event.clientX - startX.current
|
|
129
|
+
|
|
130
|
+
if (viewportRef.value) {
|
|
131
|
+
const scrollableContentHeight = viewportRef.value.scrollHeight
|
|
132
|
+
const viewportHeight = viewportRef.value.clientHeight
|
|
133
|
+
const scrollableContentWidth = viewportRef.value.scrollWidth
|
|
134
|
+
const viewportWidth = viewportRef.value.clientWidth
|
|
135
|
+
|
|
136
|
+
if (thumbYRef.value && scrollbarYRef.value && currentOrientation.current === 'vertical') {
|
|
137
|
+
const scrollbarYOffset = getOffset(scrollbarYRef.value, 'padding', 'y')
|
|
138
|
+
const thumbYOffset = getOffset(thumbYRef.value, 'margin', 'y')
|
|
139
|
+
const thumbHeight = thumbYRef.value.offsetHeight
|
|
140
|
+
const maxThumbOffsetY = scrollbarYRef.value.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset
|
|
141
|
+
const scrollRatioY = deltaY / maxThumbOffsetY
|
|
142
|
+
viewportRef.value.scrollTop = startScrollTop.current + scrollRatioY * (scrollableContentHeight - viewportHeight)
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
scrollingY.value = true
|
|
145
|
+
clearTimeout(scrollYTimeoutId)
|
|
146
|
+
scrollYTimeoutId = setTimeout(() => {
|
|
147
|
+
scrollingY.value = false
|
|
148
|
+
}, SCROLL_TIMEOUT)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (thumbXRef.value && scrollbarXRef.value && currentOrientation.current === 'horizontal') {
|
|
152
|
+
const scrollbarXOffset = getOffset(scrollbarXRef.value, 'padding', 'x')
|
|
153
|
+
const thumbXOffset = getOffset(thumbXRef.value, 'margin', 'x')
|
|
154
|
+
const thumbWidth = thumbXRef.value.offsetWidth
|
|
155
|
+
const maxThumbOffsetX = scrollbarXRef.value.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset
|
|
156
|
+
const scrollRatioX = deltaX / maxThumbOffsetX
|
|
157
|
+
viewportRef.value.scrollLeft = startScrollLeft.current + scrollRatioX * (scrollableContentWidth - viewportWidth)
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
scrollingX.value = true
|
|
160
|
+
clearTimeout(scrollXTimeoutId)
|
|
161
|
+
scrollXTimeoutId = setTimeout(() => {
|
|
162
|
+
scrollingX.value = false
|
|
163
|
+
}, SCROLL_TIMEOUT)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handlePointerUp(event: PointerEvent) {
|
|
169
|
+
thumbDragging.current = false
|
|
170
|
+
if (thumbYRef.value && currentOrientation.current === 'vertical') {
|
|
171
|
+
thumbYRef.value.releasePointerCapture(event.pointerId)
|
|
172
|
+
}
|
|
173
|
+
if (thumbXRef.value && currentOrientation.current === 'horizontal') {
|
|
174
|
+
thumbXRef.value.releasePointerCapture(event.pointerId)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function handleTouchModalityChange(event: PointerEvent) {
|
|
179
|
+
touchModality.value = event.pointerType === 'touch'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handlePointerEnterOrMove(event: PointerEvent) {
|
|
183
|
+
handleTouchModalityChange(event)
|
|
184
|
+
if (event.pointerType !== 'touch') {
|
|
185
|
+
const isTargetRootChild = contains(rootRef.value, event.target as Element)
|
|
186
|
+
hovering.value = isTargetRootChild
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const state = computed<ScrollAreaRootState>(() => ({
|
|
191
|
+
scrolling: scrollingX.value || scrollingY.value,
|
|
192
|
+
hasOverflowX: !hiddenState.value.x,
|
|
193
|
+
hasOverflowY: !hiddenState.value.y,
|
|
194
|
+
overflowXStart: overflowEdges.value.xStart,
|
|
195
|
+
overflowXEnd: overflowEdges.value.xEnd,
|
|
196
|
+
overflowYStart: overflowEdges.value.yStart,
|
|
197
|
+
overflowYEnd: overflowEdges.value.yEnd,
|
|
198
|
+
cornerHidden: hiddenState.value.corner,
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
const elementProps = computed(() => mergeProps(
|
|
202
|
+
attrs as Record<string, any>,
|
|
203
|
+
{
|
|
204
|
+
role: 'presentation',
|
|
205
|
+
onPointerenter: handlePointerEnterOrMove,
|
|
206
|
+
onPointermove: handlePointerEnterOrMove,
|
|
207
|
+
onPointerdown: handleTouchModalityChange,
|
|
208
|
+
onPointerleave: () => { hovering.value = false },
|
|
209
|
+
style: {
|
|
210
|
+
position: 'relative',
|
|
211
|
+
[ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.value.height}px`,
|
|
212
|
+
[ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.value.width}px`,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
))
|
|
216
|
+
|
|
217
|
+
const {
|
|
218
|
+
tag,
|
|
219
|
+
mergedProps,
|
|
220
|
+
renderless,
|
|
221
|
+
ref: renderRef,
|
|
222
|
+
} = useRenderElement({
|
|
223
|
+
componentProps: props,
|
|
224
|
+
state,
|
|
225
|
+
props: elementProps,
|
|
226
|
+
stateAttributesMapping: scrollAreaStateAttributesMapping,
|
|
227
|
+
defaultTagName: 'div',
|
|
228
|
+
ref: rootRef,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
onMounted(() => {
|
|
232
|
+
styleDisableScrollbar.inject(nonce.value, disableStyleElements.value)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const contextValue: ScrollAreaRootContext = {
|
|
236
|
+
cornerSize,
|
|
237
|
+
setCornerSize: (size: Size) => { cornerSize.value = size },
|
|
238
|
+
thumbSize,
|
|
239
|
+
setThumbSize: (size: Size) => { thumbSize.value = size },
|
|
240
|
+
hasMeasuredScrollbar,
|
|
241
|
+
setHasMeasuredScrollbar: (value: boolean) => { hasMeasuredScrollbar.value = value },
|
|
242
|
+
touchModality,
|
|
243
|
+
hovering,
|
|
244
|
+
setHovering: (value: boolean) => { hovering.value = value },
|
|
245
|
+
scrollingX,
|
|
246
|
+
setScrollingX: (value: boolean) => { scrollingX.value = value },
|
|
247
|
+
scrollingY,
|
|
248
|
+
setScrollingY: (value: boolean) => { scrollingY.value = value },
|
|
249
|
+
viewportRef,
|
|
250
|
+
rootRef,
|
|
251
|
+
scrollbarYRef,
|
|
252
|
+
scrollbarXRef,
|
|
253
|
+
thumbYRef,
|
|
254
|
+
thumbXRef,
|
|
255
|
+
cornerRef,
|
|
256
|
+
handlePointerDown,
|
|
257
|
+
handlePointerMove,
|
|
258
|
+
handlePointerUp,
|
|
259
|
+
handleScroll,
|
|
260
|
+
rootId,
|
|
261
|
+
hiddenState,
|
|
262
|
+
setHiddenState: (s: HiddenState) => { hiddenState.value = s },
|
|
263
|
+
overflowEdges,
|
|
264
|
+
setOverflowEdges: (e: OverflowEdges) => { overflowEdges.value = e },
|
|
265
|
+
viewportState: state,
|
|
266
|
+
overflowEdgeThreshold,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
provide(scrollAreaRootContextKey, contextValue)
|
|
270
|
+
|
|
271
|
+
function normalizeOverflowEdgeThreshold(
|
|
272
|
+
threshold: ScrollAreaRootProps['overflowEdgeThreshold'],
|
|
273
|
+
) {
|
|
274
|
+
if (typeof threshold === 'number') {
|
|
275
|
+
const value = Math.max(0, threshold)
|
|
276
|
+
return { xStart: value, xEnd: value, yStart: value, yEnd: value }
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
xStart: Math.max(0, threshold?.xStart || 0),
|
|
280
|
+
xEnd: Math.max(0, threshold?.xEnd || 0),
|
|
281
|
+
yStart: Math.max(0, threshold?.yStart || 0),
|
|
282
|
+
yEnd: Math.max(0, threshold?.yEnd || 0),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
</script>
|
|
286
|
+
|
|
287
|
+
<template>
|
|
288
|
+
<slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
|
|
289
|
+
<component
|
|
290
|
+
:is="tag"
|
|
291
|
+
v-else
|
|
292
|
+
:ref="renderRef"
|
|
293
|
+
v-bind="mergedProps"
|
|
294
|
+
>
|
|
295
|
+
<slot />
|
|
296
|
+
</component>
|
|
297
|
+
</template>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, Ref, ShallowRef } from 'vue'
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface Coords {
|
|
5
|
+
x: number
|
|
6
|
+
y: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Size {
|
|
10
|
+
width: number
|
|
11
|
+
height: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HiddenState {
|
|
15
|
+
x: boolean
|
|
16
|
+
y: boolean
|
|
17
|
+
corner: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OverflowEdges {
|
|
21
|
+
xStart: boolean
|
|
22
|
+
xEnd: boolean
|
|
23
|
+
yStart: boolean
|
|
24
|
+
yEnd: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ScrollAreaRootContext {
|
|
28
|
+
cornerSize: Ref<Size>
|
|
29
|
+
setCornerSize: (size: Size) => void
|
|
30
|
+
thumbSize: Ref<Size>
|
|
31
|
+
setThumbSize: (size: Size) => void
|
|
32
|
+
hasMeasuredScrollbar: Ref<boolean>
|
|
33
|
+
setHasMeasuredScrollbar: (value: boolean) => void
|
|
34
|
+
touchModality: Ref<boolean>
|
|
35
|
+
hovering: Ref<boolean>
|
|
36
|
+
setHovering: (value: boolean) => void
|
|
37
|
+
scrollingX: Ref<boolean>
|
|
38
|
+
setScrollingX: (value: boolean) => void
|
|
39
|
+
scrollingY: Ref<boolean>
|
|
40
|
+
setScrollingY: (value: boolean) => void
|
|
41
|
+
viewportRef: ShallowRef<HTMLDivElement | null>
|
|
42
|
+
rootRef: ShallowRef<HTMLDivElement | null>
|
|
43
|
+
scrollbarYRef: ShallowRef<HTMLDivElement | null>
|
|
44
|
+
scrollbarXRef: ShallowRef<HTMLDivElement | null>
|
|
45
|
+
thumbYRef: ShallowRef<HTMLDivElement | null>
|
|
46
|
+
thumbXRef: ShallowRef<HTMLDivElement | null>
|
|
47
|
+
cornerRef: ShallowRef<HTMLDivElement | null>
|
|
48
|
+
handlePointerDown: (event: PointerEvent) => void
|
|
49
|
+
handlePointerMove: (event: PointerEvent) => void
|
|
50
|
+
handlePointerUp: (event: PointerEvent) => void
|
|
51
|
+
handleScroll: (scrollPosition: Coords) => void
|
|
52
|
+
rootId: string | undefined
|
|
53
|
+
hiddenState: Ref<HiddenState>
|
|
54
|
+
setHiddenState: (state: HiddenState) => void
|
|
55
|
+
overflowEdges: Ref<OverflowEdges>
|
|
56
|
+
setOverflowEdges: (edges: OverflowEdges) => void
|
|
57
|
+
viewportState: ComputedRef<ScrollAreaRootState>
|
|
58
|
+
overflowEdgeThreshold: ComputedRef<{
|
|
59
|
+
xStart: number
|
|
60
|
+
xEnd: number
|
|
61
|
+
yStart: number
|
|
62
|
+
yEnd: number
|
|
63
|
+
}>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ScrollAreaRootState {
|
|
67
|
+
scrolling: boolean
|
|
68
|
+
hasOverflowX: boolean
|
|
69
|
+
hasOverflowY: boolean
|
|
70
|
+
overflowXStart: boolean
|
|
71
|
+
overflowXEnd: boolean
|
|
72
|
+
overflowYStart: boolean
|
|
73
|
+
overflowYEnd: boolean
|
|
74
|
+
cornerHidden: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const scrollAreaRootContextKey = Symbol(
|
|
78
|
+
'ScrollAreaRootContext',
|
|
79
|
+
) as InjectionKey<ScrollAreaRootContext>
|
|
80
|
+
|
|
81
|
+
export function useScrollAreaRootContext(): ScrollAreaRootContext {
|
|
82
|
+
const context = inject(scrollAreaRootContextKey)
|
|
83
|
+
if (context === undefined) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Base UI: ScrollAreaRootContext is missing. ScrollArea parts must be placed within <ScrollAreaRoot>.',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
return context
|
|
89
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export enum ScrollAreaRootDataAttributes {
|
|
2
|
+
scrolling = 'data-scrolling',
|
|
3
|
+
hasOverflowX = 'data-has-overflow-x',
|
|
4
|
+
hasOverflowY = 'data-has-overflow-y',
|
|
5
|
+
overflowXStart = 'data-overflow-x-start',
|
|
6
|
+
overflowXEnd = 'data-overflow-x-end',
|
|
7
|
+
overflowYStart = 'data-overflow-y-start',
|
|
8
|
+
overflowYEnd = 'data-overflow-y-end',
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
|
|
2
|
+
import type { ScrollAreaRootState } from './ScrollAreaRootContext'
|
|
3
|
+
import { ScrollAreaRootDataAttributes } from './ScrollAreaRootDataAttributes'
|
|
4
|
+
|
|
5
|
+
export const scrollAreaStateAttributesMapping: StateAttributesMapping<ScrollAreaRootState> = {
|
|
6
|
+
scrolling: value => (value ? { [ScrollAreaRootDataAttributes.scrolling]: '' } : null),
|
|
7
|
+
hasOverflowX: value => (value ? { [ScrollAreaRootDataAttributes.hasOverflowX]: '' } : null),
|
|
8
|
+
hasOverflowY: value => (value ? { [ScrollAreaRootDataAttributes.hasOverflowY]: '' } : null),
|
|
9
|
+
overflowXStart: value => (value ? { [ScrollAreaRootDataAttributes.overflowXStart]: '' } : null),
|
|
10
|
+
overflowXEnd: value => (value ? { [ScrollAreaRootDataAttributes.overflowXEnd]: '' } : null),
|
|
11
|
+
overflowYStart: value => (value ? { [ScrollAreaRootDataAttributes.overflowYStart]: '' } : null),
|
|
12
|
+
overflowYEnd: value => (value ? { [ScrollAreaRootDataAttributes.overflowYEnd]: '' } : null),
|
|
13
|
+
cornerHidden: () => null,
|
|
14
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ComponentPublicInstance } from 'vue'
|
|
3
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
4
|
+
import type { ScrollAreaRootState } from '../root/ScrollAreaRootContext'
|
|
5
|
+
import { computed, onBeforeUnmount, provide, shallowRef, useAttrs, watch } from 'vue'
|
|
6
|
+
import { useDirection } from '../../direction-provider/DirectionContext'
|
|
7
|
+
import { contains, getTarget } from '../../floating-ui-vue/utils/shadowDom'
|
|
8
|
+
import { mergeProps } from '../../merge-props/mergeProps'
|
|
9
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
10
|
+
import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
|
|
11
|
+
import { ScrollAreaRootCssVars } from '../root/ScrollAreaRootCssVars'
|
|
12
|
+
import { scrollAreaStateAttributesMapping } from '../root/stateAttributes'
|
|
13
|
+
import { getOffset } from '../utils/getOffset'
|
|
14
|
+
import { scrollAreaScrollbarContextKey } from './ScrollAreaScrollbarContext'
|
|
15
|
+
import { ScrollAreaScrollbarCssVars } from './ScrollAreaScrollbarCssVars'
|
|
16
|
+
|
|
17
|
+
export interface ScrollAreaScrollbarState extends ScrollAreaRootState {
|
|
18
|
+
hovering: boolean
|
|
19
|
+
scrolling: boolean
|
|
20
|
+
orientation: 'vertical' | 'horizontal'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ScrollAreaScrollbarProps extends BaseUIComponentProps<ScrollAreaScrollbarState> {
|
|
24
|
+
orientation?: 'vertical' | 'horizontal'
|
|
25
|
+
keepMounted?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
defineOptions({
|
|
29
|
+
name: 'ScrollAreaScrollbar',
|
|
30
|
+
inheritAttrs: false,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(defineProps<ScrollAreaScrollbarProps>(), {
|
|
34
|
+
as: 'div',
|
|
35
|
+
orientation: 'vertical',
|
|
36
|
+
keepMounted: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const attrs = useAttrs()
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
hovering,
|
|
43
|
+
scrollingX,
|
|
44
|
+
scrollingY,
|
|
45
|
+
hiddenState,
|
|
46
|
+
overflowEdges,
|
|
47
|
+
scrollbarYRef,
|
|
48
|
+
scrollbarXRef,
|
|
49
|
+
viewportRef,
|
|
50
|
+
thumbYRef,
|
|
51
|
+
thumbXRef,
|
|
52
|
+
handlePointerDown,
|
|
53
|
+
handlePointerUp,
|
|
54
|
+
rootId,
|
|
55
|
+
thumbSize,
|
|
56
|
+
hasMeasuredScrollbar,
|
|
57
|
+
} = useScrollAreaRootContext()
|
|
58
|
+
|
|
59
|
+
const direction = useDirection()
|
|
60
|
+
|
|
61
|
+
let wheelCleanup: (() => void) | undefined
|
|
62
|
+
const scrollbarElementRef = shallowRef<HTMLDivElement | null>(null)
|
|
63
|
+
|
|
64
|
+
watch(
|
|
65
|
+
[scrollbarElementRef, () => props.orientation],
|
|
66
|
+
([element, orientation], [previousElement, previousOrientation]) => {
|
|
67
|
+
if (previousElement) {
|
|
68
|
+
const previousRef = previousOrientation === 'vertical' ? scrollbarYRef : scrollbarXRef
|
|
69
|
+
|
|
70
|
+
if (previousRef.value === previousElement) {
|
|
71
|
+
previousRef.value = null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!element) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const nextRef = orientation === 'vertical' ? scrollbarYRef : scrollbarXRef
|
|
80
|
+
nextRef.value = element
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
watch(
|
|
85
|
+
[scrollbarElementRef, viewportRef, () => props.orientation],
|
|
86
|
+
() => {
|
|
87
|
+
wheelCleanup?.()
|
|
88
|
+
wheelCleanup = setupWheelHandler()
|
|
89
|
+
},
|
|
90
|
+
{ flush: 'post' },
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
onBeforeUnmount(() => {
|
|
94
|
+
wheelCleanup?.()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
function setupWheelHandler() {
|
|
98
|
+
const viewportEl = viewportRef.value
|
|
99
|
+
const scrollbarEl = scrollbarElementRef.value
|
|
100
|
+
|
|
101
|
+
if (!scrollbarEl)
|
|
102
|
+
return undefined
|
|
103
|
+
|
|
104
|
+
function handleWheel(event: WheelEvent) {
|
|
105
|
+
if (!viewportEl || !scrollbarEl || event.ctrlKey)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if (props.orientation === 'vertical') {
|
|
109
|
+
if (viewportEl.scrollTop === 0 && event.deltaY < 0)
|
|
110
|
+
return
|
|
111
|
+
if (viewportEl.scrollTop === viewportEl.scrollHeight - viewportEl.clientHeight && event.deltaY > 0)
|
|
112
|
+
return
|
|
113
|
+
event.preventDefault()
|
|
114
|
+
viewportEl.scrollTop += event.deltaY
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
if (viewportEl.scrollLeft === 0 && event.deltaX < 0)
|
|
118
|
+
return
|
|
119
|
+
if (viewportEl.scrollLeft === viewportEl.scrollWidth - viewportEl.clientWidth && event.deltaX > 0)
|
|
120
|
+
return
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
viewportEl.scrollLeft += event.deltaX
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
scrollbarEl.addEventListener('wheel', handleWheel, { passive: false })
|
|
127
|
+
return () => scrollbarEl.removeEventListener('wheel', handleWheel)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setScrollbarElement(element: Element | ComponentPublicInstance | null) {
|
|
131
|
+
scrollbarElementRef.value = element as HTMLDivElement | null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const state = computed<ScrollAreaScrollbarState>(() => ({
|
|
135
|
+
hovering: hovering.value,
|
|
136
|
+
scrolling: props.orientation === 'horizontal' ? scrollingX.value : scrollingY.value,
|
|
137
|
+
orientation: props.orientation,
|
|
138
|
+
hasOverflowX: !hiddenState.value.x,
|
|
139
|
+
hasOverflowY: !hiddenState.value.y,
|
|
140
|
+
overflowXStart: overflowEdges.value.xStart,
|
|
141
|
+
overflowXEnd: overflowEdges.value.xEnd,
|
|
142
|
+
overflowYStart: overflowEdges.value.yStart,
|
|
143
|
+
overflowYEnd: overflowEdges.value.yEnd,
|
|
144
|
+
cornerHidden: hiddenState.value.corner,
|
|
145
|
+
}))
|
|
146
|
+
|
|
147
|
+
const hideTrackUntilMeasured = computed(() => !hasMeasuredScrollbar.value && !props.keepMounted)
|
|
148
|
+
|
|
149
|
+
function onPointerDown(event: PointerEvent) {
|
|
150
|
+
if (event.button !== 0)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
const target = getTarget(event) as Element | null
|
|
154
|
+
const thumb = props.orientation === 'vertical' ? thumbYRef.value : thumbXRef.value
|
|
155
|
+
|
|
156
|
+
if (thumb && contains(thumb, target))
|
|
157
|
+
return
|
|
158
|
+
if (!viewportRef.value)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
if (thumbYRef.value && scrollbarYRef.value && props.orientation === 'vertical') {
|
|
162
|
+
const thumbYOffset = getOffset(thumbYRef.value, 'margin', 'y')
|
|
163
|
+
const scrollbarYOffset = getOffset(scrollbarYRef.value, 'padding', 'y')
|
|
164
|
+
const thumbHeight = thumbYRef.value.offsetHeight
|
|
165
|
+
const trackRectY = scrollbarYRef.value.getBoundingClientRect()
|
|
166
|
+
const clickY = event.clientY - trackRectY.top - thumbHeight / 2 - scrollbarYOffset + thumbYOffset / 2
|
|
167
|
+
const scrollableContentHeight = viewportRef.value.scrollHeight
|
|
168
|
+
const viewportHeight = viewportRef.value.clientHeight
|
|
169
|
+
const maxThumbOffsetY = scrollbarYRef.value.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset
|
|
170
|
+
const scrollRatioY = clickY / maxThumbOffsetY
|
|
171
|
+
viewportRef.value.scrollTop = scrollRatioY * (scrollableContentHeight - viewportHeight)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (thumbXRef.value && scrollbarXRef.value && props.orientation === 'horizontal') {
|
|
175
|
+
const thumbXOffset = getOffset(thumbXRef.value, 'margin', 'x')
|
|
176
|
+
const scrollbarXOffset = getOffset(scrollbarXRef.value, 'padding', 'x')
|
|
177
|
+
const thumbWidth = thumbXRef.value.offsetWidth
|
|
178
|
+
const trackRectX = scrollbarXRef.value.getBoundingClientRect()
|
|
179
|
+
const clickX = event.clientX - trackRectX.left - thumbWidth / 2 - scrollbarXOffset + thumbXOffset / 2
|
|
180
|
+
const scrollableContentWidth = viewportRef.value.scrollWidth
|
|
181
|
+
const viewportWidth = viewportRef.value.clientWidth
|
|
182
|
+
const maxThumbOffsetX = scrollbarXRef.value.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset
|
|
183
|
+
const scrollRatioX = clickX / maxThumbOffsetX
|
|
184
|
+
|
|
185
|
+
let newScrollLeft: number
|
|
186
|
+
if (direction.value === 'rtl') {
|
|
187
|
+
newScrollLeft = (1 - scrollRatioX) * (scrollableContentWidth - viewportWidth)
|
|
188
|
+
if (viewportRef.value.scrollLeft <= 0) {
|
|
189
|
+
newScrollLeft = -newScrollLeft
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
newScrollLeft = scrollRatioX * (scrollableContentWidth - viewportWidth)
|
|
194
|
+
}
|
|
195
|
+
viewportRef.value.scrollLeft = newScrollLeft
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
handlePointerDown(event)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const elementProps = computed(() => mergeProps(
|
|
202
|
+
attrs as Record<string, any>,
|
|
203
|
+
{
|
|
204
|
+
...(rootId ? { 'data-id': `${rootId}-scrollbar` } : {}),
|
|
205
|
+
onPointerdown: onPointerDown,
|
|
206
|
+
onPointerup: handlePointerUp,
|
|
207
|
+
style: {
|
|
208
|
+
position: 'absolute',
|
|
209
|
+
touchAction: 'none',
|
|
210
|
+
WebkitUserSelect: 'none',
|
|
211
|
+
userSelect: 'none',
|
|
212
|
+
visibility: hideTrackUntilMeasured.value ? 'hidden' : undefined,
|
|
213
|
+
...(props.orientation === 'vertical' && {
|
|
214
|
+
top: 0,
|
|
215
|
+
bottom: `var(${ScrollAreaRootCssVars.scrollAreaCornerHeight})`,
|
|
216
|
+
insetInlineEnd: 0,
|
|
217
|
+
[ScrollAreaScrollbarCssVars.scrollAreaThumbHeight]: `${thumbSize.value.height}px`,
|
|
218
|
+
}),
|
|
219
|
+
...(props.orientation === 'horizontal' && {
|
|
220
|
+
insetInlineStart: 0,
|
|
221
|
+
insetInlineEnd: `var(${ScrollAreaRootCssVars.scrollAreaCornerWidth})`,
|
|
222
|
+
bottom: 0,
|
|
223
|
+
[ScrollAreaScrollbarCssVars.scrollAreaThumbWidth]: `${thumbSize.value.width}px`,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
))
|
|
228
|
+
|
|
229
|
+
const {
|
|
230
|
+
tag,
|
|
231
|
+
mergedProps,
|
|
232
|
+
renderless,
|
|
233
|
+
ref: renderRef,
|
|
234
|
+
} = useRenderElement({
|
|
235
|
+
componentProps: props,
|
|
236
|
+
state,
|
|
237
|
+
props: elementProps,
|
|
238
|
+
stateAttributesMapping: scrollAreaStateAttributesMapping,
|
|
239
|
+
defaultTagName: 'div',
|
|
240
|
+
ref: setScrollbarElement,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const isHidden = computed(() => props.orientation === 'vertical' ? hiddenState.value.y : hiddenState.value.x)
|
|
244
|
+
const shouldRender = computed(() => props.keepMounted || !isHidden.value)
|
|
245
|
+
|
|
246
|
+
const orientation = computed(() => props.orientation)
|
|
247
|
+
|
|
248
|
+
provide(scrollAreaScrollbarContextKey, { orientation })
|
|
249
|
+
</script>
|
|
250
|
+
|
|
251
|
+
<template>
|
|
252
|
+
<template v-if="shouldRender">
|
|
253
|
+
<slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
|
|
254
|
+
<component
|
|
255
|
+
:is="tag"
|
|
256
|
+
v-else
|
|
257
|
+
:ref="renderRef"
|
|
258
|
+
v-bind="mergedProps"
|
|
259
|
+
>
|
|
260
|
+
<slot />
|
|
261
|
+
</component>
|
|
262
|
+
</template>
|
|
263
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ScrollAreaScrollbarContext {
|
|
5
|
+
orientation: ComputedRef<'horizontal' | 'vertical'>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const scrollAreaScrollbarContextKey: InjectionKey<ScrollAreaScrollbarContext> = Symbol(
|
|
9
|
+
'ScrollAreaScrollbarContext',
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
export function useScrollAreaScrollbarContext(): ScrollAreaScrollbarContext {
|
|
13
|
+
const context = inject(scrollAreaScrollbarContextKey)
|
|
14
|
+
if (context === undefined) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Base UI: ScrollAreaScrollbarContext is missing. ScrollArea.Thumb must be placed within <ScrollAreaScrollbar>.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|