base-ui-vue 0.2.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/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/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/index.cjs +33 -0
- package/dist/index.d.cts +1067 -352
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1067 -352
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index2.cjs +3565 -1373
- package/dist/index2.cjs.map +1 -1
- package/dist/index2.js +3228 -1204
- package/dist/index2.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/input/Input.vue +37 -0
- package/src/input/InputDataAttributes.ts +30 -0
- package/src/input/index.ts +4 -0
- 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/utils/formatNumber.ts +25 -0
- package/src/utils/scrollEdges.ts +33 -0
- package/src/utils/styles.ts +28 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export enum ScrollAreaScrollbarDataAttributes {
|
|
2
|
+
orientation = 'data-orientation',
|
|
3
|
+
hovering = 'data-hovering',
|
|
4
|
+
scrolling = 'data-scrolling',
|
|
5
|
+
hasOverflowX = 'data-has-overflow-x',
|
|
6
|
+
hasOverflowY = 'data-has-overflow-y',
|
|
7
|
+
overflowXStart = 'data-overflow-x-start',
|
|
8
|
+
overflowXEnd = 'data-overflow-x-end',
|
|
9
|
+
overflowYStart = 'data-overflow-y-start',
|
|
10
|
+
overflowYEnd = 'data-overflow-y-end',
|
|
11
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ComponentPublicInstance } from 'vue'
|
|
3
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
4
|
+
import { computed, shallowRef, useAttrs, watch } from 'vue'
|
|
5
|
+
import { mergeProps } from '../../merge-props/mergeProps'
|
|
6
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
7
|
+
import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
|
|
8
|
+
import { useScrollAreaScrollbarContext } from '../scrollbar/ScrollAreaScrollbarContext'
|
|
9
|
+
import { ScrollAreaScrollbarCssVars } from '../scrollbar/ScrollAreaScrollbarCssVars'
|
|
10
|
+
|
|
11
|
+
export interface ScrollAreaThumbState {
|
|
12
|
+
orientation?: 'horizontal' | 'vertical'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScrollAreaThumbProps extends BaseUIComponentProps<ScrollAreaThumbState> {}
|
|
16
|
+
|
|
17
|
+
defineOptions({
|
|
18
|
+
name: 'ScrollAreaThumb',
|
|
19
|
+
inheritAttrs: false,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<ScrollAreaThumbProps>(), {
|
|
23
|
+
as: 'div',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const attrs = useAttrs()
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
thumbYRef,
|
|
30
|
+
thumbXRef,
|
|
31
|
+
handlePointerDown,
|
|
32
|
+
handlePointerMove,
|
|
33
|
+
handlePointerUp,
|
|
34
|
+
setScrollingX,
|
|
35
|
+
setScrollingY,
|
|
36
|
+
hasMeasuredScrollbar,
|
|
37
|
+
} = useScrollAreaRootContext()
|
|
38
|
+
|
|
39
|
+
const { orientation } = useScrollAreaScrollbarContext()
|
|
40
|
+
const thumbElementRef = shallowRef<HTMLDivElement | null>(null)
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
[thumbElementRef, orientation],
|
|
44
|
+
([element, currentOrientation], [previousElement, previousOrientation]) => {
|
|
45
|
+
if (previousElement) {
|
|
46
|
+
const previousRef = previousOrientation === 'vertical' ? thumbYRef : thumbXRef
|
|
47
|
+
|
|
48
|
+
if (previousRef.value === previousElement) {
|
|
49
|
+
previousRef.value = null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!element) {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nextRef = currentOrientation === 'vertical' ? thumbYRef : thumbXRef
|
|
58
|
+
nextRef.value = element
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const state = computed<ScrollAreaThumbState>(() => ({
|
|
63
|
+
orientation: orientation.value,
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
function onPointerUp(event: PointerEvent) {
|
|
67
|
+
if (orientation.value === 'vertical') {
|
|
68
|
+
setScrollingY(false)
|
|
69
|
+
}
|
|
70
|
+
if (orientation.value === 'horizontal') {
|
|
71
|
+
setScrollingX(false)
|
|
72
|
+
}
|
|
73
|
+
handlePointerUp(event)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setThumbElement(element: Element | ComponentPublicInstance | null) {
|
|
77
|
+
thumbElementRef.value = element as HTMLDivElement | null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const elementProps = computed(() => mergeProps(
|
|
81
|
+
attrs,
|
|
82
|
+
{
|
|
83
|
+
onPointerdown: handlePointerDown,
|
|
84
|
+
onPointermove: handlePointerMove,
|
|
85
|
+
onPointerup: onPointerUp,
|
|
86
|
+
style: {
|
|
87
|
+
visibility: hasMeasuredScrollbar.value ? undefined : 'hidden',
|
|
88
|
+
...(orientation.value === 'vertical' && {
|
|
89
|
+
height: `var(${ScrollAreaScrollbarCssVars.scrollAreaThumbHeight})`,
|
|
90
|
+
}),
|
|
91
|
+
...(orientation.value === 'horizontal' && {
|
|
92
|
+
width: `var(${ScrollAreaScrollbarCssVars.scrollAreaThumbWidth})`,
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
tag,
|
|
100
|
+
mergedProps,
|
|
101
|
+
renderless,
|
|
102
|
+
ref: renderRef,
|
|
103
|
+
} = useRenderElement({
|
|
104
|
+
componentProps: props,
|
|
105
|
+
state,
|
|
106
|
+
props: elementProps,
|
|
107
|
+
defaultTagName: 'div',
|
|
108
|
+
ref: setThumbElement,
|
|
109
|
+
})
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<template>
|
|
113
|
+
<slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
|
|
114
|
+
<component
|
|
115
|
+
:is="tag"
|
|
116
|
+
v-else
|
|
117
|
+
:ref="renderRef"
|
|
118
|
+
v-bind="mergedProps"
|
|
119
|
+
/>
|
|
120
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function getOffset(
|
|
2
|
+
element: Element | null,
|
|
3
|
+
prop: 'margin' | 'padding',
|
|
4
|
+
axis: 'x' | 'y',
|
|
5
|
+
): number {
|
|
6
|
+
if (!element) {
|
|
7
|
+
return 0
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const styles = getComputedStyle(element)
|
|
11
|
+
const propAxis = axis === 'x' ? 'Inline' : 'Block'
|
|
12
|
+
const start = getStyleNumber(styles, `${prop}${propAxis}Start`)
|
|
13
|
+
const end = getStyleNumber(styles, `${prop}${propAxis}End`)
|
|
14
|
+
|
|
15
|
+
// Safari misreports `marginInlineEnd` in RTL.
|
|
16
|
+
// We have to assume the start/end values are symmetrical, which is likely.
|
|
17
|
+
if (axis === 'x' && prop === 'margin' && styles.direction === 'rtl' && isSafari()) {
|
|
18
|
+
return start * 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return start + end
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getStyleNumber(styles: CSSStyleDeclaration, prop: string) {
|
|
25
|
+
return Number.parseFloat(styles[prop as any]) || 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isSafari() {
|
|
29
|
+
if (typeof navigator === 'undefined') {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return /AppleWebKit/.test(navigator.userAgent) && !/Chrome|Chromium|Edg\//.test(navigator.userAgent)
|
|
34
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
3
|
+
import type { HiddenState, ScrollAreaRootState } from '../root/ScrollAreaRootContext'
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, provide, useAttrs, watch } from 'vue'
|
|
5
|
+
import { useDirection } from '../../direction-provider/DirectionContext'
|
|
6
|
+
import { mergeProps } from '../../merge-props/mergeProps'
|
|
7
|
+
import { clamp } from '../../utils/clamp'
|
|
8
|
+
import { normalizeScrollOffset } from '../../utils/scrollEdges'
|
|
9
|
+
import { styleDisableScrollbar } from '../../utils/styles'
|
|
10
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
11
|
+
import { MIN_THUMB_SIZE } from '../constants'
|
|
12
|
+
import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
|
|
13
|
+
import { scrollAreaStateAttributesMapping } from '../root/stateAttributes'
|
|
14
|
+
import { getOffset } from '../utils/getOffset'
|
|
15
|
+
import { scrollAreaViewportContextKey } from './ScrollAreaViewportContext'
|
|
16
|
+
import { ScrollAreaViewportCssVars } from './ScrollAreaViewportCssVars'
|
|
17
|
+
|
|
18
|
+
export type ScrollAreaViewportState = ScrollAreaRootState
|
|
19
|
+
|
|
20
|
+
export interface ScrollAreaViewportProps extends BaseUIComponentProps<ScrollAreaViewportState> {}
|
|
21
|
+
|
|
22
|
+
defineOptions({
|
|
23
|
+
name: 'ScrollAreaViewport',
|
|
24
|
+
inheritAttrs: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<ScrollAreaViewportProps>(), {
|
|
28
|
+
as: 'div',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const attrs = useAttrs()
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
viewportRef,
|
|
35
|
+
scrollbarYRef,
|
|
36
|
+
scrollbarXRef,
|
|
37
|
+
thumbYRef,
|
|
38
|
+
thumbXRef,
|
|
39
|
+
cornerRef,
|
|
40
|
+
cornerSize,
|
|
41
|
+
setCornerSize,
|
|
42
|
+
setThumbSize,
|
|
43
|
+
rootId,
|
|
44
|
+
setHiddenState,
|
|
45
|
+
hiddenState,
|
|
46
|
+
setHasMeasuredScrollbar,
|
|
47
|
+
handleScroll,
|
|
48
|
+
setHovering,
|
|
49
|
+
setOverflowEdges,
|
|
50
|
+
overflowEdges,
|
|
51
|
+
overflowEdgeThreshold,
|
|
52
|
+
scrollingX,
|
|
53
|
+
scrollingY,
|
|
54
|
+
} = useScrollAreaRootContext()
|
|
55
|
+
|
|
56
|
+
const direction = useDirection()
|
|
57
|
+
|
|
58
|
+
let programmaticScroll = true
|
|
59
|
+
const lastMeasuredViewportMetrics: [number, number, number, number] = [Number.NaN, Number.NaN, Number.NaN, Number.NaN]
|
|
60
|
+
|
|
61
|
+
let scrollEndTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
62
|
+
let waitForAnimationsTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
63
|
+
let resizeObserver: ResizeObserver | undefined
|
|
64
|
+
|
|
65
|
+
let scrollAreaOverflowVarsRegistered = false
|
|
66
|
+
|
|
67
|
+
function removeCSSVariableInheritance() {
|
|
68
|
+
if (scrollAreaOverflowVarsRegistered)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
const isWebKit = typeof navigator !== 'undefined' && /AppleWebKit/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
|
|
72
|
+
if (isWebKit)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
|
|
76
|
+
const vars = [
|
|
77
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowXStart,
|
|
78
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowXEnd,
|
|
79
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowYStart,
|
|
80
|
+
ScrollAreaViewportCssVars.scrollAreaOverflowYEnd,
|
|
81
|
+
]
|
|
82
|
+
for (const name of vars) {
|
|
83
|
+
try {
|
|
84
|
+
CSS.registerProperty({ name, syntax: '<length>', inherits: false, initialValue: '0px' })
|
|
85
|
+
}
|
|
86
|
+
catch { /* already registered */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
scrollAreaOverflowVarsRegistered = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function computeThumbPosition() {
|
|
94
|
+
const viewportEl = viewportRef.value
|
|
95
|
+
const scrollbarYEl = scrollbarYRef.value
|
|
96
|
+
const scrollbarXEl = scrollbarXRef.value
|
|
97
|
+
const thumbYEl = thumbYRef.value
|
|
98
|
+
const thumbXEl = thumbXRef.value
|
|
99
|
+
const cornerEl = cornerRef.value
|
|
100
|
+
|
|
101
|
+
if (!viewportEl)
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
const scrollableContentHeight = viewportEl.scrollHeight
|
|
105
|
+
const scrollableContentWidth = viewportEl.scrollWidth
|
|
106
|
+
const viewportHeight = viewportEl.clientHeight
|
|
107
|
+
const viewportWidth = viewportEl.clientWidth
|
|
108
|
+
const scrollTop = viewportEl.scrollTop
|
|
109
|
+
const scrollLeft = viewportEl.scrollLeft
|
|
110
|
+
const isFirstMeasurement = Number.isNaN(lastMeasuredViewportMetrics[0])
|
|
111
|
+
lastMeasuredViewportMetrics[0] = viewportHeight
|
|
112
|
+
lastMeasuredViewportMetrics[1] = scrollableContentHeight
|
|
113
|
+
lastMeasuredViewportMetrics[2] = viewportWidth
|
|
114
|
+
lastMeasuredViewportMetrics[3] = scrollableContentWidth
|
|
115
|
+
|
|
116
|
+
if (isFirstMeasurement) {
|
|
117
|
+
setHasMeasuredScrollbar(true)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (scrollableContentHeight === 0 || scrollableContentWidth === 0)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
const nextHiddenState = getHiddenState(viewportEl)
|
|
124
|
+
const scrollbarYHidden = nextHiddenState.y
|
|
125
|
+
const scrollbarXHidden = nextHiddenState.x
|
|
126
|
+
const ratioX = viewportWidth / scrollableContentWidth
|
|
127
|
+
const ratioY = viewportHeight / scrollableContentHeight
|
|
128
|
+
const maxScrollLeft = Math.max(0, scrollableContentWidth - viewportWidth)
|
|
129
|
+
const maxScrollTop = Math.max(0, scrollableContentHeight - viewportHeight)
|
|
130
|
+
|
|
131
|
+
let scrollLeftFromStart = 0
|
|
132
|
+
let scrollLeftFromEnd = 0
|
|
133
|
+
if (!scrollbarXHidden) {
|
|
134
|
+
let rawScrollLeftFromStart = 0
|
|
135
|
+
if (direction.value === 'rtl') {
|
|
136
|
+
rawScrollLeftFromStart = scrollLeft < 0 ? -scrollLeft : maxScrollLeft - scrollLeft
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
rawScrollLeftFromStart = scrollLeft
|
|
140
|
+
}
|
|
141
|
+
rawScrollLeftFromStart = clamp(rawScrollLeftFromStart, 0, maxScrollLeft)
|
|
142
|
+
scrollLeftFromStart = normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft)
|
|
143
|
+
scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const rawScrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0
|
|
147
|
+
const scrollTopFromStart = !scrollbarYHidden ? normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0
|
|
148
|
+
const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0
|
|
149
|
+
const nextWidth = scrollbarXHidden ? 0 : viewportWidth
|
|
150
|
+
const nextHeight = scrollbarYHidden ? 0 : viewportHeight
|
|
151
|
+
|
|
152
|
+
let nextCornerWidth = 0
|
|
153
|
+
let nextCornerHeight = 0
|
|
154
|
+
if (!scrollbarXHidden && !scrollbarYHidden) {
|
|
155
|
+
nextCornerWidth = scrollbarYEl?.offsetWidth || 0
|
|
156
|
+
nextCornerHeight = scrollbarXEl?.offsetHeight || 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cornerNotYetSized = cornerSize.value.width === 0 && cornerSize.value.height === 0
|
|
160
|
+
const cornerWidthOffset = cornerNotYetSized ? nextCornerWidth : 0
|
|
161
|
+
const cornerHeightOffset = cornerNotYetSized ? nextCornerHeight : 0
|
|
162
|
+
|
|
163
|
+
const scrollbarXOffset = getOffset(scrollbarXEl, 'padding', 'x')
|
|
164
|
+
const scrollbarYOffset = getOffset(scrollbarYEl, 'padding', 'y')
|
|
165
|
+
const thumbXOffset = getOffset(thumbXEl, 'margin', 'x')
|
|
166
|
+
const thumbYOffset = getOffset(thumbYEl, 'margin', 'y')
|
|
167
|
+
|
|
168
|
+
const idealNextWidth = nextWidth - scrollbarXOffset - thumbXOffset
|
|
169
|
+
const idealNextHeight = nextHeight - scrollbarYOffset - thumbYOffset
|
|
170
|
+
|
|
171
|
+
const maxNextWidth = scrollbarXEl ? Math.min(scrollbarXEl.offsetWidth - cornerWidthOffset, idealNextWidth) : idealNextWidth
|
|
172
|
+
const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight
|
|
173
|
+
|
|
174
|
+
const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX)
|
|
175
|
+
const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY)
|
|
176
|
+
|
|
177
|
+
setThumbSize({ width: clampedNextWidth, height: clampedNextHeight })
|
|
178
|
+
|
|
179
|
+
if (scrollbarYEl && thumbYEl) {
|
|
180
|
+
const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset
|
|
181
|
+
const scrollRangeY = scrollableContentHeight - viewportHeight
|
|
182
|
+
const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY
|
|
183
|
+
const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY))
|
|
184
|
+
thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (scrollbarXEl && thumbXEl) {
|
|
188
|
+
const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset
|
|
189
|
+
const scrollRangeX = scrollableContentWidth - viewportWidth
|
|
190
|
+
const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeftFromStart / scrollRangeX
|
|
191
|
+
const thumbOffsetX = direction.value === 'rtl'
|
|
192
|
+
? -clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX)
|
|
193
|
+
: clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX)
|
|
194
|
+
thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const overflowMetricsPx: Array<[string, number]> = [
|
|
198
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart],
|
|
199
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd],
|
|
200
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart],
|
|
201
|
+
[ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd],
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
for (const [cssVar, value] of overflowMetricsPx) {
|
|
205
|
+
viewportEl.style.setProperty(cssVar, `${value}px`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (cornerEl) {
|
|
209
|
+
if (scrollbarXHidden || scrollbarYHidden) {
|
|
210
|
+
setCornerSize({ width: 0, height: 0 })
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
setCornerSize({ width: nextCornerWidth, height: nextCornerHeight })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
setHiddenState(mergeHiddenState(hiddenState.value, nextHiddenState))
|
|
218
|
+
|
|
219
|
+
const threshold = overflowEdgeThreshold.value
|
|
220
|
+
const nextOverflowEdges = {
|
|
221
|
+
xStart: !scrollbarXHidden && scrollLeftFromStart > threshold.xStart,
|
|
222
|
+
xEnd: !scrollbarXHidden && scrollLeftFromEnd > threshold.xEnd,
|
|
223
|
+
yStart: !scrollbarYHidden && scrollTopFromStart > threshold.yStart,
|
|
224
|
+
yEnd: !scrollbarYHidden && scrollTopFromEnd > threshold.yEnd,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const prev = overflowEdges.value
|
|
228
|
+
if (
|
|
229
|
+
prev.xStart !== nextOverflowEdges.xStart
|
|
230
|
+
|| prev.xEnd !== nextOverflowEdges.xEnd
|
|
231
|
+
|| prev.yStart !== nextOverflowEdges.yStart
|
|
232
|
+
|| prev.yEnd !== nextOverflowEdges.yEnd
|
|
233
|
+
) {
|
|
234
|
+
setOverflowEdges(nextOverflowEdges)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function handleUserInteraction() {
|
|
239
|
+
programmaticScroll = false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function onScroll() {
|
|
243
|
+
if (!viewportRef.value)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
computeThumbPosition()
|
|
247
|
+
|
|
248
|
+
if (!programmaticScroll) {
|
|
249
|
+
handleScroll({
|
|
250
|
+
x: viewportRef.value.scrollLeft,
|
|
251
|
+
y: viewportRef.value.scrollTop,
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
clearTimeout(scrollEndTimeoutId)
|
|
256
|
+
scrollEndTimeoutId = setTimeout(() => {
|
|
257
|
+
programmaticScroll = true
|
|
258
|
+
}, 100)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const viewportState = computed<ScrollAreaViewportState>(() => ({
|
|
262
|
+
scrolling: scrollingX.value || scrollingY.value,
|
|
263
|
+
hasOverflowX: !hiddenState.value.x,
|
|
264
|
+
hasOverflowY: !hiddenState.value.y,
|
|
265
|
+
overflowXStart: overflowEdges.value.xStart,
|
|
266
|
+
overflowXEnd: overflowEdges.value.xEnd,
|
|
267
|
+
overflowYStart: overflowEdges.value.yStart,
|
|
268
|
+
overflowYEnd: overflowEdges.value.yEnd,
|
|
269
|
+
cornerHidden: hiddenState.value.corner,
|
|
270
|
+
}))
|
|
271
|
+
|
|
272
|
+
const elementProps = computed(() => mergeProps(
|
|
273
|
+
attrs as Record<string, any>,
|
|
274
|
+
{
|
|
275
|
+
role: 'presentation',
|
|
276
|
+
...(rootId ? { 'data-id': `${rootId}-viewport` } : {}),
|
|
277
|
+
tabindex: hiddenState.value.x && hiddenState.value.y ? -1 : 0,
|
|
278
|
+
class: styleDisableScrollbar.className,
|
|
279
|
+
style: { overflow: 'scroll' },
|
|
280
|
+
onScroll,
|
|
281
|
+
onWheel: handleUserInteraction,
|
|
282
|
+
onTouchmove: handleUserInteraction,
|
|
283
|
+
onPointermove: handleUserInteraction,
|
|
284
|
+
onPointerenter: handleUserInteraction,
|
|
285
|
+
onKeydown: handleUserInteraction,
|
|
286
|
+
},
|
|
287
|
+
))
|
|
288
|
+
|
|
289
|
+
const {
|
|
290
|
+
tag,
|
|
291
|
+
mergedProps,
|
|
292
|
+
renderless,
|
|
293
|
+
ref: renderRef,
|
|
294
|
+
} = useRenderElement({
|
|
295
|
+
componentProps: props,
|
|
296
|
+
state: viewportState,
|
|
297
|
+
props: elementProps,
|
|
298
|
+
stateAttributesMapping: scrollAreaStateAttributesMapping,
|
|
299
|
+
defaultTagName: 'div',
|
|
300
|
+
ref: viewportRef,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
onMounted(() => {
|
|
304
|
+
removeCSSVariableInheritance()
|
|
305
|
+
|
|
306
|
+
if (viewportRef.value?.matches(':hover')) {
|
|
307
|
+
setHovering(true)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
queueMicrotask(computeThumbPosition)
|
|
311
|
+
|
|
312
|
+
const viewport = viewportRef.value
|
|
313
|
+
if (typeof ResizeObserver !== 'undefined' && viewport) {
|
|
314
|
+
let hasInitialized = false
|
|
315
|
+
resizeObserver = new ResizeObserver(() => {
|
|
316
|
+
if (!hasInitialized) {
|
|
317
|
+
hasInitialized = true
|
|
318
|
+
if (
|
|
319
|
+
lastMeasuredViewportMetrics[0] === viewport.clientHeight
|
|
320
|
+
&& lastMeasuredViewportMetrics[1] === viewport.scrollHeight
|
|
321
|
+
&& lastMeasuredViewportMetrics[2] === viewport.clientWidth
|
|
322
|
+
&& lastMeasuredViewportMetrics[3] === viewport.scrollWidth
|
|
323
|
+
) {
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
computeThumbPosition()
|
|
328
|
+
})
|
|
329
|
+
resizeObserver.observe(viewport)
|
|
330
|
+
|
|
331
|
+
waitForAnimationsTimeoutId = setTimeout(() => {
|
|
332
|
+
const animations = viewport.getAnimations({ subtree: true })
|
|
333
|
+
if (animations.length === 0)
|
|
334
|
+
return
|
|
335
|
+
Promise.allSettled(animations.map(a => a.finished))
|
|
336
|
+
.then(computeThumbPosition)
|
|
337
|
+
}, 0)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
watch([hiddenState, direction], () => {
|
|
342
|
+
queueMicrotask(computeThumbPosition)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
watch(overflowEdgeThreshold, computeThumbPosition)
|
|
346
|
+
|
|
347
|
+
onBeforeUnmount(() => {
|
|
348
|
+
resizeObserver?.disconnect()
|
|
349
|
+
clearTimeout(scrollEndTimeoutId)
|
|
350
|
+
clearTimeout(waitForAnimationsTimeoutId)
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
provide(scrollAreaViewportContextKey, { computeThumbPosition })
|
|
354
|
+
|
|
355
|
+
function getHiddenState(viewport: HTMLElement): HiddenState {
|
|
356
|
+
const y = viewport.clientHeight >= viewport.scrollHeight
|
|
357
|
+
const x = viewport.clientWidth >= viewport.scrollWidth
|
|
358
|
+
return { y, x, corner: y || x }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function mergeHiddenState(prevState: HiddenState, nextState: HiddenState): HiddenState {
|
|
362
|
+
if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
|
|
363
|
+
return prevState
|
|
364
|
+
}
|
|
365
|
+
return nextState
|
|
366
|
+
}
|
|
367
|
+
</script>
|
|
368
|
+
|
|
369
|
+
<template>
|
|
370
|
+
<slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="viewportState" />
|
|
371
|
+
<component
|
|
372
|
+
:is="tag"
|
|
373
|
+
v-else
|
|
374
|
+
:ref="renderRef"
|
|
375
|
+
v-bind="mergedProps"
|
|
376
|
+
>
|
|
377
|
+
<slot />
|
|
378
|
+
</component>
|
|
379
|
+
</template>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
import { inject } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ScrollAreaViewportContext {
|
|
5
|
+
computeThumbPosition: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const scrollAreaViewportContextKey = Symbol(
|
|
9
|
+
'ScrollAreaViewportContext',
|
|
10
|
+
) as InjectionKey<ScrollAreaViewportContext>
|
|
11
|
+
|
|
12
|
+
export function useScrollAreaViewportContext(): ScrollAreaViewportContext {
|
|
13
|
+
const context = inject(scrollAreaViewportContextKey)
|
|
14
|
+
if (context === undefined) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Base UI: ScrollAreaViewportContext is missing. ScrollArea.Content must be placed within <ScrollAreaViewport>.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return context
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export enum ScrollAreaViewportCssVars {
|
|
2
|
+
scrollAreaOverflowXStart = '--scroll-area-overflow-x-start',
|
|
3
|
+
scrollAreaOverflowXEnd = '--scroll-area-overflow-x-end',
|
|
4
|
+
scrollAreaOverflowYStart = '--scroll-area-overflow-y-start',
|
|
5
|
+
scrollAreaOverflowYEnd = '--scroll-area-overflow-y-end',
|
|
6
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export enum ScrollAreaViewportDataAttributes {
|
|
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
|
+
}
|
|
@@ -5,3 +5,28 @@ export function formatNumber(
|
|
|
5
5
|
) {
|
|
6
6
|
return new Intl.NumberFormat(locale, options).format(value)
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Formats a numeric value for display inside Base UI Vue components.
|
|
11
|
+
*
|
|
12
|
+
* When no `format` is provided, the value is interpreted as a percentage
|
|
13
|
+
* in the 0-100 range (matching React's Base UI semantics for
|
|
14
|
+
* `<Meter.Root>` / `<Progress.Root>`).
|
|
15
|
+
*
|
|
16
|
+
* Returns an empty string when the value is `null`.
|
|
17
|
+
*/
|
|
18
|
+
export function formatNumberValue(
|
|
19
|
+
value: number | null,
|
|
20
|
+
locale?: Intl.LocalesArgument,
|
|
21
|
+
format?: Intl.NumberFormatOptions,
|
|
22
|
+
): string {
|
|
23
|
+
if (value == null) {
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!format) {
|
|
28
|
+
return formatNumber(value / 100, locale, { style: 'percent' })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return formatNumber(value, locale, format)
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { clamp } from './clamp'
|
|
2
|
+
|
|
3
|
+
export const SCROLL_EDGE_TOLERANCE_PX = 1
|
|
4
|
+
|
|
5
|
+
export function getMaxScrollOffset(scrollSize: number, clientSize: number) {
|
|
6
|
+
return Math.max(0, scrollSize - clientSize)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeScrollOffset(value: number, max: number) {
|
|
10
|
+
if (max <= 0) {
|
|
11
|
+
return 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const clamped = clamp(value, 0, max)
|
|
15
|
+
const startDistance = clamped
|
|
16
|
+
const endDistance = max - clamped
|
|
17
|
+
const withinStartTolerance = startDistance <= SCROLL_EDGE_TOLERANCE_PX
|
|
18
|
+
const withinEndTolerance = endDistance <= SCROLL_EDGE_TOLERANCE_PX
|
|
19
|
+
|
|
20
|
+
if (withinStartTolerance && withinEndTolerance) {
|
|
21
|
+
return startDistance <= endDistance ? 0 : max
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (withinStartTolerance) {
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (withinEndTolerance) {
|
|
29
|
+
return max
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return clamped
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const DISABLE_SCROLLBAR_CLASS_NAME = 'base-ui-disable-scrollbar'
|
|
2
|
+
const STYLE_ELEMENT_ID = `style-${DISABLE_SCROLLBAR_CLASS_NAME}`
|
|
3
|
+
|
|
4
|
+
export const styleDisableScrollbar = {
|
|
5
|
+
className: DISABLE_SCROLLBAR_CLASS_NAME,
|
|
6
|
+
|
|
7
|
+
inject(nonce?: string, disableStyleElements?: boolean) {
|
|
8
|
+
if (disableStyleElements) {
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof document === 'undefined') {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (document.getElementById(STYLE_ELEMENT_ID)) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const style = document.createElement('style')
|
|
21
|
+
style.id = STYLE_ELEMENT_ID
|
|
22
|
+
if (nonce) {
|
|
23
|
+
style.nonce = nonce
|
|
24
|
+
}
|
|
25
|
+
style.textContent = `.${DISABLE_SCROLLBAR_CLASS_NAME}{scrollbar-width:none}.${DISABLE_SCROLLBAR_CLASS_NAME}::-webkit-scrollbar{display:none}`
|
|
26
|
+
document.head.appendChild(style)
|
|
27
|
+
},
|
|
28
|
+
}
|