base-ui-vue 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/button/ToolbarButton.cjs +6 -0
  2. package/dist/button/ToolbarButton.js +1 -1
  3. package/dist/content/ScrollAreaContent.cjs +168 -0
  4. package/dist/content/ScrollAreaContent.cjs.map +1 -0
  5. package/dist/content/ScrollAreaContent.js +133 -0
  6. package/dist/content/ScrollAreaContent.js.map +1 -0
  7. package/dist/control/SliderControl.js +2 -2
  8. package/dist/corner/ScrollAreaCorner.cjs +77 -0
  9. package/dist/corner/ScrollAreaCorner.cjs.map +1 -0
  10. package/dist/corner/ScrollAreaCorner.js +72 -0
  11. package/dist/corner/ScrollAreaCorner.js.map +1 -0
  12. package/dist/decrement/NumberFieldDecrement.cjs +861 -0
  13. package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
  14. package/dist/decrement/NumberFieldDecrement.js +700 -0
  15. package/dist/decrement/NumberFieldDecrement.js.map +1 -0
  16. package/dist/fallback/AvatarFallback.cjs +2 -46
  17. package/dist/fallback/AvatarFallback.cjs.map +1 -1
  18. package/dist/fallback/AvatarFallback.js +3 -41
  19. package/dist/fallback/AvatarFallback.js.map +1 -1
  20. package/dist/group/NumberFieldGroup.cjs +72 -0
  21. package/dist/group/NumberFieldGroup.cjs.map +1 -0
  22. package/dist/group/NumberFieldGroup.js +67 -0
  23. package/dist/group/NumberFieldGroup.js.map +1 -0
  24. package/dist/increment/NumberFieldIncrement.cjs +112 -0
  25. package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
  26. package/dist/increment/NumberFieldIncrement.js +107 -0
  27. package/dist/increment/NumberFieldIncrement.js.map +1 -0
  28. package/dist/index.cjs +52 -0
  29. package/dist/index.d.cts +1761 -430
  30. package/dist/index.d.cts.map +1 -1
  31. package/dist/index.d.ts +1761 -430
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -2
  34. package/dist/index2.cjs +4065 -60
  35. package/dist/index2.cjs.map +1 -1
  36. package/dist/index2.js +3955 -184
  37. package/dist/index2.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/index.ts +6 -0
  40. package/src/input/Input.vue +37 -0
  41. package/src/input/InputDataAttributes.ts +30 -0
  42. package/src/input/index.ts +4 -0
  43. package/src/meter/index.ts +16 -0
  44. package/src/meter/indicator/MeterIndicator.vue +65 -0
  45. package/src/meter/label/MeterLabel.vue +63 -0
  46. package/src/meter/root/MeterRoot.vue +131 -0
  47. package/src/meter/root/MeterRootContext.ts +41 -0
  48. package/src/meter/track/MeterTrack.vue +46 -0
  49. package/src/meter/value/MeterValue.vue +85 -0
  50. package/src/number-field/decrement/NumberFieldDecrement.vue +109 -0
  51. package/src/number-field/group/NumberFieldGroup.vue +47 -0
  52. package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
  53. package/src/number-field/index.ts +42 -0
  54. package/src/number-field/input/NumberFieldInput.vue +455 -0
  55. package/src/number-field/root/NumberFieldRoot.vue +626 -0
  56. package/src/number-field/root/NumberFieldRootContext.ts +94 -0
  57. package/src/number-field/root/useNumberFieldButton.ts +171 -0
  58. package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
  59. package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
  60. package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
  61. package/src/number-field/utils/constants.ts +4 -0
  62. package/src/number-field/utils/getViewportRect.ts +34 -0
  63. package/src/number-field/utils/parse.ts +248 -0
  64. package/src/number-field/utils/stateAttributesMapping.ts +9 -0
  65. package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
  66. package/src/number-field/utils/types.ts +24 -0
  67. package/src/number-field/utils/validate.ts +120 -0
  68. package/src/otp-field/index.ts +22 -0
  69. package/src/otp-field/input/OtpFieldInput.vue +336 -0
  70. package/src/otp-field/root/OtpFieldRoot.vue +583 -0
  71. package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
  72. package/src/otp-field/utils/otp.ts +135 -0
  73. package/src/otp-field/utils/stateAttributesMapping.ts +16 -0
  74. package/src/progress/index.ts +23 -0
  75. package/src/progress/indicator/ProgressIndicator.vue +74 -0
  76. package/src/progress/label/ProgressLabel.vue +63 -0
  77. package/src/progress/root/ProgressRoot.vue +160 -0
  78. package/src/progress/root/ProgressRootContext.ts +51 -0
  79. package/src/progress/root/ProgressRootDataAttributes.ts +14 -0
  80. package/src/progress/root/stateAttributesMapping.ts +18 -0
  81. package/src/progress/track/ProgressTrack.vue +48 -0
  82. package/src/progress/value/ProgressValue.vue +92 -0
  83. package/src/scroll-area/constants.ts +2 -0
  84. package/src/scroll-area/content/ScrollAreaContent.vue +87 -0
  85. package/src/scroll-area/corner/ScrollAreaCorner.vue +64 -0
  86. package/src/scroll-area/index.ts +25 -0
  87. package/src/scroll-area/root/ScrollAreaRoot.vue +297 -0
  88. package/src/scroll-area/root/ScrollAreaRootContext.ts +89 -0
  89. package/src/scroll-area/root/ScrollAreaRootCssVars.ts +4 -0
  90. package/src/scroll-area/root/ScrollAreaRootDataAttributes.ts +9 -0
  91. package/src/scroll-area/root/stateAttributes.ts +14 -0
  92. package/src/scroll-area/scrollbar/ScrollAreaScrollbar.vue +263 -0
  93. package/src/scroll-area/scrollbar/ScrollAreaScrollbarContext.ts +20 -0
  94. package/src/scroll-area/scrollbar/ScrollAreaScrollbarCssVars.ts +4 -0
  95. package/src/scroll-area/scrollbar/ScrollAreaScrollbarDataAttributes.ts +11 -0
  96. package/src/scroll-area/thumb/ScrollAreaThumb.vue +120 -0
  97. package/src/scroll-area/thumb/ScrollAreaThumbDataAttributes.ts +3 -0
  98. package/src/scroll-area/utils/getOffset.ts +34 -0
  99. package/src/scroll-area/viewport/ScrollAreaViewport.vue +379 -0
  100. package/src/scroll-area/viewport/ScrollAreaViewportContext.ts +20 -0
  101. package/src/scroll-area/viewport/ScrollAreaViewportCssVars.ts +6 -0
  102. package/src/scroll-area/viewport/ScrollAreaViewportDataAttributes.ts +9 -0
  103. package/src/utils/detectBrowser.ts +15 -0
  104. package/src/utils/formatNumber.ts +60 -2
  105. package/src/utils/scrollEdges.ts +33 -0
  106. package/src/utils/styles.ts +28 -0
  107. package/src/utils/useInterval.ts +45 -0
  108. package/src/utils/usePressAndHold.ts +260 -0
  109. package/src/utils/useValueChanged.ts +21 -0
@@ -0,0 +1,92 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import type { ProgressRootState } from '../root/ProgressRoot.vue'
4
+ import { computed, useAttrs } from 'vue'
5
+ import { useRenderElement } from '../../utils/useRenderElement'
6
+ import { useProgressRootContext } from '../root/ProgressRootContext'
7
+ import { progressStateAttributesMapping } from '../root/stateAttributesMapping'
8
+
9
+ export interface ProgressValueState extends ProgressRootState {}
10
+ export interface ProgressValueProps extends BaseUIComponentProps<ProgressValueState> {}
11
+
12
+ export interface ProgressValueSlotProps {
13
+ /**
14
+ * Formatted value. `"indeterminate"` when the root value is `null`.
15
+ */
16
+ formattedValue: string | 'indeterminate'
17
+ /**
18
+ * Raw numeric value, or `null` when indeterminate.
19
+ */
20
+ value: number | null
21
+ }
22
+
23
+ export interface ProgressValueRenderlessSlotProps extends ProgressValueSlotProps {
24
+ props: Record<string, unknown>
25
+ state: ProgressValueState
26
+ }
27
+
28
+ /**
29
+ * A text label displaying the current value.
30
+ * Renders a `<span>` element.
31
+ *
32
+ * Documentation: [Base UI Vue Progress](https://baseui-vue.com/docs/components/progress)
33
+ */
34
+ defineOptions({
35
+ name: 'ProgressValue',
36
+ inheritAttrs: false,
37
+ })
38
+
39
+ const props = withDefaults(defineProps<ProgressValueProps>(), {
40
+ as: 'span',
41
+ })
42
+
43
+ defineSlots<{
44
+ default?: (props: ProgressValueSlotProps | ProgressValueRenderlessSlotProps) => unknown
45
+ }>()
46
+
47
+ const attrs = useAttrs()
48
+ const { value, formattedValue, state } = useProgressRootContext()
49
+
50
+ const slotFormattedValue = computed<string | 'indeterminate'>(() =>
51
+ value.value == null ? 'indeterminate' : formattedValue.value,
52
+ )
53
+
54
+ const defaultDisplay = computed(() =>
55
+ value.value == null ? '' : formattedValue.value,
56
+ )
57
+
58
+ const valueProps = computed(() => ({
59
+ 'aria-hidden': true,
60
+ ...attrs,
61
+ }))
62
+
63
+ const {
64
+ tag,
65
+ mergedProps,
66
+ renderless,
67
+ } = useRenderElement({
68
+ componentProps: props,
69
+ state,
70
+ props: valueProps,
71
+ defaultTagName: 'span',
72
+ stateAttributesMapping: progressStateAttributesMapping,
73
+ })
74
+ </script>
75
+
76
+ <template>
77
+ <slot
78
+ v-if="renderless"
79
+ :props="mergedProps"
80
+ :state="state"
81
+ :formatted-value="slotFormattedValue"
82
+ :value="value"
83
+ />
84
+ <component :is="tag" v-else v-bind="mergedProps">
85
+ <slot
86
+ :formatted-value="slotFormattedValue"
87
+ :value="value"
88
+ >
89
+ {{ defaultDisplay }}
90
+ </slot>
91
+ </component>
92
+ </template>
@@ -0,0 +1,2 @@
1
+ export const SCROLL_TIMEOUT = 500
2
+ export const MIN_THUMB_SIZE = 16
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import type { ScrollAreaRootState } from '../root/ScrollAreaRootContext'
4
+ import { computed, onBeforeUnmount, onMounted, shallowRef, useAttrs } from 'vue'
5
+ import { mergeProps } from '../../merge-props/mergeProps'
6
+ import { useRenderElement } from '../../utils/useRenderElement'
7
+ import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
8
+ import { scrollAreaStateAttributesMapping } from '../root/stateAttributes'
9
+ import { useScrollAreaViewportContext } from '../viewport/ScrollAreaViewportContext'
10
+
11
+ export type ScrollAreaContentState = ScrollAreaRootState
12
+
13
+ export interface ScrollAreaContentProps extends BaseUIComponentProps<ScrollAreaContentState> {}
14
+
15
+ defineOptions({
16
+ name: 'ScrollAreaContent',
17
+ inheritAttrs: false,
18
+ })
19
+
20
+ const props = withDefaults(defineProps<ScrollAreaContentProps>(), {
21
+ as: 'div',
22
+ })
23
+
24
+ const attrs = useAttrs()
25
+
26
+ const contentWrapperRef = shallowRef<HTMLDivElement | null>(null)
27
+ const { computeThumbPosition } = useScrollAreaViewportContext()
28
+ const { viewportState } = useScrollAreaRootContext()
29
+
30
+ let resizeObserver: ResizeObserver | undefined
31
+
32
+ onMounted(() => {
33
+ if (typeof ResizeObserver === 'undefined')
34
+ return
35
+
36
+ let hasInitialized = false
37
+ resizeObserver = new ResizeObserver(() => {
38
+ if (!hasInitialized) {
39
+ hasInitialized = true
40
+ return
41
+ }
42
+ computeThumbPosition()
43
+ })
44
+
45
+ if (contentWrapperRef.value) {
46
+ resizeObserver.observe(contentWrapperRef.value)
47
+ }
48
+ })
49
+
50
+ onBeforeUnmount(() => {
51
+ resizeObserver?.disconnect()
52
+ })
53
+
54
+ const elementProps = computed(() => mergeProps(
55
+ attrs as Record<string, any>,
56
+ {
57
+ role: 'presentation',
58
+ style: { minWidth: 'fit-content' },
59
+ },
60
+ ))
61
+
62
+ const {
63
+ tag,
64
+ mergedProps,
65
+ renderless,
66
+ ref: renderRef,
67
+ } = useRenderElement({
68
+ componentProps: props,
69
+ state: viewportState,
70
+ props: elementProps,
71
+ stateAttributesMapping: scrollAreaStateAttributesMapping,
72
+ defaultTagName: 'div',
73
+ ref: contentWrapperRef,
74
+ })
75
+ </script>
76
+
77
+ <template>
78
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="viewportState" />
79
+ <component
80
+ :is="tag"
81
+ v-else
82
+ :ref="renderRef"
83
+ v-bind="mergedProps"
84
+ >
85
+ <slot />
86
+ </component>
87
+ </template>
@@ -0,0 +1,64 @@
1
+ <script setup lang="ts">
2
+ import type { BaseUIComponentProps } from '../../utils/types'
3
+ import { computed, useAttrs } from 'vue'
4
+ import { mergeProps } from '../../merge-props/mergeProps'
5
+ import { useRenderElement } from '../../utils/useRenderElement'
6
+ import { useScrollAreaRootContext } from '../root/ScrollAreaRootContext'
7
+
8
+ export interface ScrollAreaCornerState {}
9
+
10
+ export interface ScrollAreaCornerProps extends BaseUIComponentProps<ScrollAreaCornerState> {}
11
+
12
+ defineOptions({
13
+ name: 'ScrollAreaCorner',
14
+ inheritAttrs: false,
15
+ })
16
+
17
+ const props = withDefaults(defineProps<ScrollAreaCornerProps>(), {
18
+ as: 'div',
19
+ })
20
+
21
+ const attrs = useAttrs()
22
+
23
+ const { cornerRef, cornerSize, hiddenState } = useScrollAreaRootContext()
24
+
25
+ const state = computed<ScrollAreaCornerState>(() => ({}))
26
+
27
+ const elementProps = computed(() => mergeProps(
28
+ attrs as Record<string, any>,
29
+ {
30
+ style: {
31
+ position: 'absolute',
32
+ bottom: 0,
33
+ insetInlineEnd: 0,
34
+ width: cornerSize.value.width,
35
+ height: cornerSize.value.height,
36
+ },
37
+ },
38
+ ))
39
+
40
+ const {
41
+ tag,
42
+ mergedProps,
43
+ renderless,
44
+ ref: renderRef,
45
+ } = useRenderElement({
46
+ componentProps: props,
47
+ state,
48
+ props: elementProps,
49
+ defaultTagName: 'div',
50
+ ref: cornerRef,
51
+ })
52
+ </script>
53
+
54
+ <template>
55
+ <template v-if="!hiddenState.corner">
56
+ <slot v-if="renderless" :ref="renderRef" :props="mergedProps" :state="state" />
57
+ <component
58
+ :is="tag"
59
+ v-else
60
+ :ref="renderRef"
61
+ v-bind="mergedProps"
62
+ />
63
+ </template>
64
+ </template>
@@ -0,0 +1,25 @@
1
+ export { default as ScrollAreaContent } from './content/ScrollAreaContent.vue'
2
+ export type { ScrollAreaContentProps, ScrollAreaContentState } from './content/ScrollAreaContent.vue'
3
+ export { default as ScrollAreaCorner } from './corner/ScrollAreaCorner.vue'
4
+ export type { ScrollAreaCornerProps, ScrollAreaCornerState } from './corner/ScrollAreaCorner.vue'
5
+ export { default as ScrollAreaRoot } from './root/ScrollAreaRoot.vue'
6
+
7
+ export type { ScrollAreaRootProps } from './root/ScrollAreaRoot.vue'
8
+ export type { ScrollAreaRootState } from './root/ScrollAreaRootContext'
9
+ export { ScrollAreaRootCssVars } from './root/ScrollAreaRootCssVars'
10
+ export { ScrollAreaRootDataAttributes } from './root/ScrollAreaRootDataAttributes'
11
+
12
+ export { default as ScrollAreaScrollbar } from './scrollbar/ScrollAreaScrollbar.vue'
13
+ export type { ScrollAreaScrollbarProps, ScrollAreaScrollbarState } from './scrollbar/ScrollAreaScrollbar.vue'
14
+
15
+ export { ScrollAreaScrollbarCssVars } from './scrollbar/ScrollAreaScrollbarCssVars'
16
+ export { ScrollAreaScrollbarDataAttributes } from './scrollbar/ScrollAreaScrollbarDataAttributes'
17
+ export { default as ScrollAreaThumb } from './thumb/ScrollAreaThumb.vue'
18
+ export type { ScrollAreaThumbProps, ScrollAreaThumbState } from './thumb/ScrollAreaThumb.vue'
19
+
20
+ export { ScrollAreaThumbDataAttributes } from './thumb/ScrollAreaThumbDataAttributes'
21
+ export { default as ScrollAreaViewport } from './viewport/ScrollAreaViewport.vue'
22
+ export type { ScrollAreaViewportProps, ScrollAreaViewportState } from './viewport/ScrollAreaViewport.vue'
23
+
24
+ export { ScrollAreaViewportCssVars } from './viewport/ScrollAreaViewportCssVars'
25
+ export { ScrollAreaViewportDataAttributes } from './viewport/ScrollAreaViewportDataAttributes'
@@ -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,4 @@
1
+ export enum ScrollAreaRootCssVars {
2
+ scrollAreaCornerHeight = '--scroll-area-corner-height',
3
+ scrollAreaCornerWidth = '--scroll-area-corner-width',
4
+ }
@@ -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
+ }