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.
- package/dist/button/ToolbarButton.cjs +6 -0
- package/dist/button/ToolbarButton.js +1 -1
- 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/SliderControl.js +2 -2
- 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/decrement/NumberFieldDecrement.cjs +861 -0
- package/dist/decrement/NumberFieldDecrement.cjs.map +1 -0
- package/dist/decrement/NumberFieldDecrement.js +700 -0
- package/dist/decrement/NumberFieldDecrement.js.map +1 -0
- package/dist/fallback/AvatarFallback.cjs +2 -46
- package/dist/fallback/AvatarFallback.cjs.map +1 -1
- package/dist/fallback/AvatarFallback.js +3 -41
- package/dist/fallback/AvatarFallback.js.map +1 -1
- package/dist/group/NumberFieldGroup.cjs +72 -0
- package/dist/group/NumberFieldGroup.cjs.map +1 -0
- package/dist/group/NumberFieldGroup.js +67 -0
- package/dist/group/NumberFieldGroup.js.map +1 -0
- package/dist/increment/NumberFieldIncrement.cjs +112 -0
- package/dist/increment/NumberFieldIncrement.cjs.map +1 -0
- package/dist/increment/NumberFieldIncrement.js +107 -0
- package/dist/increment/NumberFieldIncrement.js.map +1 -0
- package/dist/index.cjs +52 -0
- package/dist/index.d.cts +1761 -430
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1761 -430
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index2.cjs +4065 -60
- package/dist/index2.cjs.map +1 -1
- package/dist/index2.js +3955 -184
- package/dist/index2.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -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/number-field/decrement/NumberFieldDecrement.vue +109 -0
- package/src/number-field/group/NumberFieldGroup.vue +47 -0
- package/src/number-field/increment/NumberFieldIncrement.vue +109 -0
- package/src/number-field/index.ts +42 -0
- package/src/number-field/input/NumberFieldInput.vue +455 -0
- package/src/number-field/root/NumberFieldRoot.vue +626 -0
- package/src/number-field/root/NumberFieldRootContext.ts +94 -0
- package/src/number-field/root/useNumberFieldButton.ts +171 -0
- package/src/number-field/scrub-area/NumberFieldScrubArea.vue +359 -0
- package/src/number-field/scrub-area/NumberFieldScrubAreaContext.ts +26 -0
- package/src/number-field/scrub-area-cursor/NumberFieldScrubAreaCursor.vue +75 -0
- package/src/number-field/utils/constants.ts +4 -0
- package/src/number-field/utils/getViewportRect.ts +34 -0
- package/src/number-field/utils/parse.ts +248 -0
- package/src/number-field/utils/stateAttributesMapping.ts +9 -0
- package/src/number-field/utils/subscribeToVisualViewportResize.ts +27 -0
- package/src/number-field/utils/types.ts +24 -0
- package/src/number-field/utils/validate.ts +120 -0
- package/src/otp-field/index.ts +22 -0
- package/src/otp-field/input/OtpFieldInput.vue +336 -0
- package/src/otp-field/root/OtpFieldRoot.vue +583 -0
- package/src/otp-field/root/OtpFieldRootContext.ts +81 -0
- package/src/otp-field/utils/otp.ts +135 -0
- package/src/otp-field/utils/stateAttributesMapping.ts +16 -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/detectBrowser.ts +15 -0
- package/src/utils/formatNumber.ts +60 -2
- package/src/utils/scrollEdges.ts +33 -0
- package/src/utils/styles.ts +28 -0
- package/src/utils/useInterval.ts +45 -0
- package/src/utils/usePressAndHold.ts +260 -0
- package/src/utils/useValueChanged.ts +21 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
interface OTPValidationConfig {
|
|
2
|
+
slotPattern: string
|
|
3
|
+
getRootPattern: (length: number) => string
|
|
4
|
+
regexp: RegExp
|
|
5
|
+
inputMode: 'numeric' | 'text'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type OtpValidationType = 'numeric' | 'alpha' | 'alphanumeric' | 'none'
|
|
9
|
+
|
|
10
|
+
const OTP_VALIDATION_CONFIG: Record<Exclude<OtpValidationType, 'none'>, OTPValidationConfig> = {
|
|
11
|
+
numeric: {
|
|
12
|
+
slotPattern: '\\d{1}',
|
|
13
|
+
getRootPattern: length => `\\d{${length}}`,
|
|
14
|
+
regexp: /\D/g,
|
|
15
|
+
inputMode: 'numeric',
|
|
16
|
+
},
|
|
17
|
+
alpha: {
|
|
18
|
+
slotPattern: '[a-z]{1}',
|
|
19
|
+
getRootPattern: length => `[a-z]{${length}}`,
|
|
20
|
+
regexp: /[^a-z]/gi,
|
|
21
|
+
inputMode: 'text',
|
|
22
|
+
},
|
|
23
|
+
alphanumeric: {
|
|
24
|
+
slotPattern: '[a-z0-9]{1}',
|
|
25
|
+
getRootPattern: length => `[a-z0-9]{${length}}`,
|
|
26
|
+
regexp: /[^a-z0-9]/gi,
|
|
27
|
+
inputMode: 'text',
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getOTPValidationConfig(validationType: OtpValidationType) {
|
|
32
|
+
if (validationType === 'none') {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return OTP_VALIDATION_CONFIG[validationType]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stripOTPWhitespace(value: string | null | undefined) {
|
|
40
|
+
return (value ?? '').replace(/\s/g, '')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function applyOTPValidation(value: string, validation: OTPValidationConfig | null) {
|
|
44
|
+
return validation ? value.replace(validation.regexp, '') : value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes user-entered OTP text by stripping whitespace, applying validation and custom
|
|
49
|
+
* normalization, and clamping the final value to the configured slot count.
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeOTPValueWithDetails(
|
|
52
|
+
value: string | null | undefined,
|
|
53
|
+
length: number,
|
|
54
|
+
validationType: OtpValidationType,
|
|
55
|
+
normalizeValue?: ((value: string) => string) | undefined,
|
|
56
|
+
): readonly [value: string, didRejectCharacters: boolean] {
|
|
57
|
+
const strippedValue = stripOTPWhitespace(value)
|
|
58
|
+
const validation = getOTPValidationConfig(validationType)
|
|
59
|
+
let normalizedValue = applyOTPValidation(strippedValue, validation)
|
|
60
|
+
let didRejectCharacters = strippedValue.length > normalizedValue.length
|
|
61
|
+
|
|
62
|
+
if (normalizeValue) {
|
|
63
|
+
const customNormalizedValue = normalizeValue(normalizedValue)
|
|
64
|
+
didRejectCharacters ||= normalizedValue.length > customNormalizedValue.length
|
|
65
|
+
normalizedValue = applyOTPValidation(customNormalizedValue, validation)
|
|
66
|
+
didRejectCharacters ||= customNormalizedValue.length > normalizedValue.length
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Slice by Unicode code points so multi-byte characters do not split across OTP slots.
|
|
70
|
+
const maxLength = length < 0 ? 0 : length
|
|
71
|
+
const normalizedCharacters = Array.from(normalizedValue)
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
normalizedCharacters.slice(0, maxLength).join(''),
|
|
75
|
+
didRejectCharacters || normalizedCharacters.length > maxLength,
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeOTPValue(
|
|
80
|
+
value: string | null | undefined,
|
|
81
|
+
length: number,
|
|
82
|
+
validationType: OtpValidationType,
|
|
83
|
+
normalizeValue?: ((value: string) => string) | undefined,
|
|
84
|
+
) {
|
|
85
|
+
return normalizeOTPValueWithDetails(value, length, validationType, normalizeValue)[0]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getOTPCharacters(value: string | null | undefined) {
|
|
89
|
+
return Array.from(value ?? '')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getOTPValueLength(value: string | null | undefined) {
|
|
93
|
+
return getOTPCharacters(value).length
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getOTPCharacter(value: string | null | undefined, index: number) {
|
|
97
|
+
return getOTPCharacters(value)[index] ?? ''
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Replaces characters starting at the provided slot index, then re-normalizes the final OTP value
|
|
102
|
+
* so paste and multi-character edits stay contiguous and length-bounded.
|
|
103
|
+
*/
|
|
104
|
+
export function replaceOTPValue(
|
|
105
|
+
currentValue: string,
|
|
106
|
+
index: number,
|
|
107
|
+
nextValue: string,
|
|
108
|
+
length: number,
|
|
109
|
+
validationType: OtpValidationType,
|
|
110
|
+
normalizeValue?: ((value: string) => string) | undefined,
|
|
111
|
+
) {
|
|
112
|
+
const normalizedValue = normalizeOTPValue(nextValue, length, validationType, normalizeValue)
|
|
113
|
+
const characters = getOTPCharacters(currentValue)
|
|
114
|
+
const replacementLength = getOTPValueLength(normalizedValue)
|
|
115
|
+
const prefix = characters.slice(0, index).join('')
|
|
116
|
+
const suffix = characters.slice(index + replacementLength).join('')
|
|
117
|
+
|
|
118
|
+
return normalizeOTPValue(
|
|
119
|
+
`${prefix}${normalizedValue}${suffix}`,
|
|
120
|
+
length,
|
|
121
|
+
validationType,
|
|
122
|
+
normalizeValue,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function removeOTPCharacter(currentValue: string, index: number) {
|
|
127
|
+
const characters = getOTPCharacters(currentValue)
|
|
128
|
+
|
|
129
|
+
if (index < 0 || index >= characters.length) {
|
|
130
|
+
return currentValue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
characters.splice(index, 1)
|
|
134
|
+
return characters.join('')
|
|
135
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
|
|
2
|
+
import type { OtpFieldInputState } from '../input/OtpFieldInput.vue'
|
|
3
|
+
import type { OtpFieldRootState } from '../root/OtpFieldRoot.vue'
|
|
4
|
+
import { fieldValidityMapping } from '../../field/utils/constants'
|
|
5
|
+
|
|
6
|
+
export const rootStateAttributesMapping: StateAttributesMapping<OtpFieldRootState> = {
|
|
7
|
+
value: () => null,
|
|
8
|
+
length: () => null,
|
|
9
|
+
...fieldValidityMapping,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const inputStateAttributesMapping: StateAttributesMapping<OtpFieldInputState> = {
|
|
13
|
+
value: () => null,
|
|
14
|
+
index: () => null,
|
|
15
|
+
...fieldValidityMapping,
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { default as ProgressIndicator } from './indicator/ProgressIndicator.vue'
|
|
2
|
+
export type { ProgressIndicatorProps, ProgressIndicatorState } from './indicator/ProgressIndicator.vue'
|
|
3
|
+
|
|
4
|
+
export { default as ProgressLabel } from './label/ProgressLabel.vue'
|
|
5
|
+
export type { ProgressLabelProps, ProgressLabelState } from './label/ProgressLabel.vue'
|
|
6
|
+
|
|
7
|
+
export { default as ProgressRoot } from './root/ProgressRoot.vue'
|
|
8
|
+
export type { ProgressRootProps, ProgressRootState, ProgressStatus } from './root/ProgressRoot.vue'
|
|
9
|
+
export { progressRootContextKey, useProgressRootContext } from './root/ProgressRootContext'
|
|
10
|
+
export type { ProgressRootContext } from './root/ProgressRootContext'
|
|
11
|
+
export { ProgressRootDataAttributes } from './root/ProgressRootDataAttributes'
|
|
12
|
+
export { progressStateAttributesMapping } from './root/stateAttributesMapping'
|
|
13
|
+
|
|
14
|
+
export { default as ProgressTrack } from './track/ProgressTrack.vue'
|
|
15
|
+
export type { ProgressTrackProps, ProgressTrackState } from './track/ProgressTrack.vue'
|
|
16
|
+
|
|
17
|
+
export { default as ProgressValue } from './value/ProgressValue.vue'
|
|
18
|
+
export type {
|
|
19
|
+
ProgressValueProps,
|
|
20
|
+
ProgressValueRenderlessSlotProps,
|
|
21
|
+
ProgressValueSlotProps,
|
|
22
|
+
ProgressValueState,
|
|
23
|
+
} from './value/ProgressValue.vue'
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { mergeProps } from '../../merge-props/mergeProps'
|
|
6
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
7
|
+
import { valueToPercent } from '../../utils/valueToPercent'
|
|
8
|
+
import { useProgressRootContext } from '../root/ProgressRootContext'
|
|
9
|
+
import { progressStateAttributesMapping } from '../root/stateAttributesMapping'
|
|
10
|
+
|
|
11
|
+
export interface ProgressIndicatorState extends ProgressRootState {}
|
|
12
|
+
export interface ProgressIndicatorProps extends BaseUIComponentProps<ProgressIndicatorState> {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Visualizes the completion status of the task.
|
|
16
|
+
* Renders a `<div>` element.
|
|
17
|
+
*
|
|
18
|
+
* Documentation: [Base UI Vue Progress](https://baseui-vue.com/docs/components/progress)
|
|
19
|
+
*/
|
|
20
|
+
defineOptions({
|
|
21
|
+
name: 'ProgressIndicator',
|
|
22
|
+
inheritAttrs: false,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const props = withDefaults(defineProps<ProgressIndicatorProps>(), {
|
|
26
|
+
as: 'div',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const attrs = useAttrs()
|
|
30
|
+
const { value, min, max, state } = useProgressRootContext()
|
|
31
|
+
|
|
32
|
+
const percentage = computed(() => {
|
|
33
|
+
const v = value.value
|
|
34
|
+
if (v == null || !Number.isFinite(v)) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
return valueToPercent(v, min.value, max.value)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const indicatorStyles = computed(() => {
|
|
41
|
+
if (percentage.value == null) {
|
|
42
|
+
return {}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
insetInlineStart: 0,
|
|
46
|
+
height: 'inherit',
|
|
47
|
+
width: `${percentage.value}%`,
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const indicatorProps = computed(() => mergeProps(
|
|
52
|
+
{ style: indicatorStyles.value },
|
|
53
|
+
attrs as Record<string, unknown>,
|
|
54
|
+
))
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
tag,
|
|
58
|
+
mergedProps,
|
|
59
|
+
renderless,
|
|
60
|
+
} = useRenderElement({
|
|
61
|
+
componentProps: props,
|
|
62
|
+
state,
|
|
63
|
+
props: indicatorProps,
|
|
64
|
+
defaultTagName: 'div',
|
|
65
|
+
stateAttributesMapping: progressStateAttributesMapping,
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<slot v-if="renderless" :props="mergedProps" :state="state" />
|
|
71
|
+
<component :is="tag" v-else v-bind="mergedProps">
|
|
72
|
+
<slot :state="state" />
|
|
73
|
+
</component>
|
|
74
|
+
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { useRegisteredLabelId } from '../../utils/useRegisteredLabelId'
|
|
6
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
7
|
+
import { useProgressRootContext } from '../root/ProgressRootContext'
|
|
8
|
+
import { progressStateAttributesMapping } from '../root/stateAttributesMapping'
|
|
9
|
+
|
|
10
|
+
export interface ProgressLabelState extends ProgressRootState {}
|
|
11
|
+
export interface ProgressLabelProps extends BaseUIComponentProps<ProgressLabelState> {
|
|
12
|
+
/**
|
|
13
|
+
* The id of the label element. When provided, it overrides the
|
|
14
|
+
* automatically generated one.
|
|
15
|
+
*/
|
|
16
|
+
id?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An accessible label for the progress bar.
|
|
21
|
+
* Renders a `<span>` element.
|
|
22
|
+
*
|
|
23
|
+
* Documentation: [Base UI Vue Progress](https://baseui-vue.com/docs/components/progress)
|
|
24
|
+
*/
|
|
25
|
+
defineOptions({
|
|
26
|
+
name: 'ProgressLabel',
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<ProgressLabelProps>(), {
|
|
31
|
+
as: 'span',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const attrs = useAttrs()
|
|
35
|
+
const { setLabelId, state } = useProgressRootContext()
|
|
36
|
+
|
|
37
|
+
const id = useRegisteredLabelId(() => props.id, setLabelId)
|
|
38
|
+
|
|
39
|
+
const labelProps = computed(() => ({
|
|
40
|
+
...attrs,
|
|
41
|
+
id: id.value,
|
|
42
|
+
role: 'presentation',
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
const {
|
|
46
|
+
tag,
|
|
47
|
+
mergedProps,
|
|
48
|
+
renderless,
|
|
49
|
+
} = useRenderElement({
|
|
50
|
+
componentProps: props,
|
|
51
|
+
state,
|
|
52
|
+
props: labelProps,
|
|
53
|
+
defaultTagName: 'span',
|
|
54
|
+
stateAttributesMapping: progressStateAttributesMapping,
|
|
55
|
+
})
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<template>
|
|
59
|
+
<slot v-if="renderless" :props="mergedProps" :state="state" />
|
|
60
|
+
<component :is="tag" v-else v-bind="mergedProps">
|
|
61
|
+
<slot :state="state" />
|
|
62
|
+
</component>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
3
|
+
import { computed, provide, ref, useAttrs } from 'vue'
|
|
4
|
+
import { formatNumberValue } from '../../utils/formatNumber'
|
|
5
|
+
import { useRenderElement } from '../../utils/useRenderElement'
|
|
6
|
+
import { visuallyHidden } from '../../utils/visuallyHidden'
|
|
7
|
+
import { progressRootContextKey } from './ProgressRootContext'
|
|
8
|
+
import { progressStateAttributesMapping } from './stateAttributesMapping'
|
|
9
|
+
|
|
10
|
+
export type ProgressStatus = 'indeterminate' | 'progressing' | 'complete'
|
|
11
|
+
|
|
12
|
+
export interface ProgressRootState {
|
|
13
|
+
/**
|
|
14
|
+
* The current status.
|
|
15
|
+
*/
|
|
16
|
+
status: ProgressStatus
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProgressRootProps extends BaseUIComponentProps<ProgressRootState> {
|
|
20
|
+
/**
|
|
21
|
+
* A string value that provides a user-friendly name for `aria-valuenow`.
|
|
22
|
+
* Takes precedence over `getAriaValueText`.
|
|
23
|
+
*/
|
|
24
|
+
ariaValuetext?: string
|
|
25
|
+
/**
|
|
26
|
+
* Options to format the value.
|
|
27
|
+
*/
|
|
28
|
+
format?: Intl.NumberFormatOptions
|
|
29
|
+
/**
|
|
30
|
+
* A function that returns a string value for `aria-valuetext`.
|
|
31
|
+
* Receives the formatted value (or `null` when indeterminate) and the raw
|
|
32
|
+
* value.
|
|
33
|
+
*/
|
|
34
|
+
getAriaValueText?: (formattedValue: string | null, value: number | null) => string
|
|
35
|
+
/**
|
|
36
|
+
* The locale used by `Intl.NumberFormat` when formatting the value.
|
|
37
|
+
* Defaults to the user's runtime locale.
|
|
38
|
+
*/
|
|
39
|
+
locale?: Intl.LocalesArgument
|
|
40
|
+
/**
|
|
41
|
+
* The maximum value.
|
|
42
|
+
* @default 100
|
|
43
|
+
*/
|
|
44
|
+
max?: number
|
|
45
|
+
/**
|
|
46
|
+
* The minimum value.
|
|
47
|
+
* @default 0
|
|
48
|
+
*/
|
|
49
|
+
min?: number
|
|
50
|
+
/**
|
|
51
|
+
* The current value. The component is indeterminate when value is `null`.
|
|
52
|
+
* @default null
|
|
53
|
+
*/
|
|
54
|
+
value: number | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Groups all parts of the progress bar and provides the task completion
|
|
59
|
+
* status to screen readers.
|
|
60
|
+
* Renders a `<div>` element.
|
|
61
|
+
*
|
|
62
|
+
* Documentation: [Base UI Vue Progress](https://baseui-vue.com/docs/components/progress)
|
|
63
|
+
*/
|
|
64
|
+
defineOptions({
|
|
65
|
+
name: 'ProgressRoot',
|
|
66
|
+
inheritAttrs: false,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const props = withDefaults(defineProps<ProgressRootProps>(), {
|
|
70
|
+
as: 'div',
|
|
71
|
+
max: 100,
|
|
72
|
+
min: 0,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
function getDefaultAriaValueText(formattedValue: string | null, value: number | null) {
|
|
76
|
+
if (value == null) {
|
|
77
|
+
return 'indeterminate progress'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return formattedValue || `${value}%`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const attrs = useAttrs()
|
|
84
|
+
|
|
85
|
+
const labelId = ref<string | undefined>(undefined)
|
|
86
|
+
|
|
87
|
+
const status = computed<ProgressStatus>(() => {
|
|
88
|
+
if (props.value == null || !Number.isFinite(props.value)) {
|
|
89
|
+
return 'indeterminate'
|
|
90
|
+
}
|
|
91
|
+
return props.value === props.max ? 'complete' : 'progressing'
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const formattedValue = computed(() =>
|
|
95
|
+
formatNumberValue(props.value, props.locale, props.format),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
const ariaValueText = computed(() => {
|
|
99
|
+
if (props.ariaValuetext !== undefined) {
|
|
100
|
+
return props.ariaValuetext
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const formatted = props.value == null ? null : formattedValue.value
|
|
104
|
+
|
|
105
|
+
if (props.getAriaValueText) {
|
|
106
|
+
return props.getAriaValueText(formatted, props.value)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return getDefaultAriaValueText(formatted, props.value)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const state = computed<ProgressRootState>(() => ({
|
|
113
|
+
status: status.value,
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
provide(progressRootContextKey, {
|
|
117
|
+
formattedValue,
|
|
118
|
+
max: computed(() => props.max),
|
|
119
|
+
min: computed(() => props.min),
|
|
120
|
+
value: computed(() => props.value),
|
|
121
|
+
status,
|
|
122
|
+
state,
|
|
123
|
+
setLabelId(id) {
|
|
124
|
+
labelId.value = id
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const rootProps = computed(() => ({
|
|
129
|
+
'role': 'progressbar',
|
|
130
|
+
'aria-labelledby': labelId.value,
|
|
131
|
+
'aria-valuemax': props.max,
|
|
132
|
+
'aria-valuemin': props.min,
|
|
133
|
+
'aria-valuenow': props.value ?? undefined,
|
|
134
|
+
'aria-valuetext': ariaValueText.value,
|
|
135
|
+
...attrs,
|
|
136
|
+
}))
|
|
137
|
+
|
|
138
|
+
const {
|
|
139
|
+
tag,
|
|
140
|
+
mergedProps,
|
|
141
|
+
renderless,
|
|
142
|
+
} = useRenderElement({
|
|
143
|
+
componentProps: props,
|
|
144
|
+
state,
|
|
145
|
+
props: rootProps,
|
|
146
|
+
defaultTagName: 'div',
|
|
147
|
+
stateAttributesMapping: progressStateAttributesMapping,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const visuallyHiddenStyle = visuallyHidden
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<slot v-if="renderless" :props="mergedProps" :state="state" />
|
|
155
|
+
<component :is="tag" v-else v-bind="mergedProps">
|
|
156
|
+
<slot :state="state" />
|
|
157
|
+
<!-- Force NVDA to read the label https://github.com/mui/base-ui/issues/4184 -->
|
|
158
|
+
<span role="presentation" :style="visuallyHiddenStyle">x</span>
|
|
159
|
+
</component>
|
|
160
|
+
</template>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey, Ref } from 'vue'
|
|
2
|
+
import type { ProgressRootState, ProgressStatus } from './ProgressRoot.vue'
|
|
3
|
+
import { inject } from 'vue'
|
|
4
|
+
|
|
5
|
+
export interface ProgressRootContext {
|
|
6
|
+
/**
|
|
7
|
+
* The formatted current value of the progress bar.
|
|
8
|
+
* Empty string when the component is indeterminate.
|
|
9
|
+
*/
|
|
10
|
+
formattedValue: Ref<string>
|
|
11
|
+
/**
|
|
12
|
+
* The maximum allowed value of the progress bar.
|
|
13
|
+
*/
|
|
14
|
+
max: Ref<number>
|
|
15
|
+
/**
|
|
16
|
+
* The minimum allowed value of the progress bar.
|
|
17
|
+
*/
|
|
18
|
+
min: Ref<number>
|
|
19
|
+
/**
|
|
20
|
+
* The raw current value, or `null` when indeterminate.
|
|
21
|
+
*/
|
|
22
|
+
value: Ref<number | null>
|
|
23
|
+
/**
|
|
24
|
+
* Derived completion status. Used to produce `data-*` state attributes.
|
|
25
|
+
*/
|
|
26
|
+
status: ComputedRef<ProgressStatus>
|
|
27
|
+
/**
|
|
28
|
+
* The state of the root component.
|
|
29
|
+
*/
|
|
30
|
+
state: ComputedRef<ProgressRootState>
|
|
31
|
+
/**
|
|
32
|
+
* Registers the DOM id of the `<ProgressLabel>` for `aria-labelledby`.
|
|
33
|
+
*/
|
|
34
|
+
setLabelId: (id: string | undefined) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const progressRootContextKey: InjectionKey<ProgressRootContext>
|
|
38
|
+
= Symbol('ProgressRootContext')
|
|
39
|
+
|
|
40
|
+
export function useProgressRootContext(optional: true): ProgressRootContext | undefined
|
|
41
|
+
export function useProgressRootContext(optional?: false): ProgressRootContext
|
|
42
|
+
export function useProgressRootContext(optional = false) {
|
|
43
|
+
const context = inject(progressRootContextKey, undefined)
|
|
44
|
+
if (!context && !optional) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Base UI Vue: ProgressRootContext is missing. Progress parts must be placed within <ProgressRoot>.',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return context
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export enum ProgressRootDataAttributes {
|
|
2
|
+
/**
|
|
3
|
+
* Present when the progress has completed.
|
|
4
|
+
*/
|
|
5
|
+
complete = 'data-complete',
|
|
6
|
+
/**
|
|
7
|
+
* Present when the progress is in indeterminate state.
|
|
8
|
+
*/
|
|
9
|
+
indeterminate = 'data-indeterminate',
|
|
10
|
+
/**
|
|
11
|
+
* Present while the progress is progressing.
|
|
12
|
+
*/
|
|
13
|
+
progressing = 'data-progressing',
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StateAttributesMapping } from '../../utils/getStateAttributesProps'
|
|
2
|
+
import type { ProgressRootState } from './ProgressRoot.vue'
|
|
3
|
+
import { ProgressRootDataAttributes } from './ProgressRootDataAttributes'
|
|
4
|
+
|
|
5
|
+
export const progressStateAttributesMapping: StateAttributesMapping<ProgressRootState> = {
|
|
6
|
+
status(value): Record<string, string> | null {
|
|
7
|
+
if (value === 'progressing') {
|
|
8
|
+
return { [ProgressRootDataAttributes.progressing]: '' }
|
|
9
|
+
}
|
|
10
|
+
if (value === 'complete') {
|
|
11
|
+
return { [ProgressRootDataAttributes.complete]: '' }
|
|
12
|
+
}
|
|
13
|
+
if (value === 'indeterminate') {
|
|
14
|
+
return { [ProgressRootDataAttributes.indeterminate]: '' }
|
|
15
|
+
}
|
|
16
|
+
return null
|
|
17
|
+
},
|
|
18
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { BaseUIComponentProps } from '../../utils/types'
|
|
3
|
+
import type { ProgressRootState } from '../root/ProgressRoot.vue'
|
|
4
|
+
import { 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 ProgressTrackState extends ProgressRootState {}
|
|
10
|
+
export interface ProgressTrackProps extends BaseUIComponentProps<ProgressTrackState> {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Contains the progress bar indicator.
|
|
14
|
+
* Renders a `<div>` element.
|
|
15
|
+
*
|
|
16
|
+
* Documentation: [Base UI Vue Progress](https://baseui-vue.com/docs/components/progress)
|
|
17
|
+
*/
|
|
18
|
+
defineOptions({
|
|
19
|
+
name: 'ProgressTrack',
|
|
20
|
+
inheritAttrs: false,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const props = withDefaults(defineProps<ProgressTrackProps>(), {
|
|
24
|
+
as: 'div',
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const attrs = useAttrs()
|
|
28
|
+
const { state } = useProgressRootContext()
|
|
29
|
+
|
|
30
|
+
const {
|
|
31
|
+
tag,
|
|
32
|
+
mergedProps,
|
|
33
|
+
renderless,
|
|
34
|
+
} = useRenderElement({
|
|
35
|
+
componentProps: props,
|
|
36
|
+
state,
|
|
37
|
+
props: attrs,
|
|
38
|
+
defaultTagName: 'div',
|
|
39
|
+
stateAttributesMapping: progressStateAttributesMapping,
|
|
40
|
+
})
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<slot v-if="renderless" :props="mergedProps" :state="state" />
|
|
45
|
+
<component :is="tag" v-else v-bind="mergedProps">
|
|
46
|
+
<slot :state="state" />
|
|
47
|
+
</component>
|
|
48
|
+
</template>
|