Skip to content

feat(UProgress): add circular variant #4120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: v3
Choose a base branch
from
4 changes: 4 additions & 0 deletions docs/content/1.getting-started/3.theme.md
Original file line number Diff line number Diff line change
Expand Up @@ -746,8 +746,12 @@ This is how the `@theme` is generated for each design token:
--outline-color-default: var(--ui-border);
--outline-color-inverted: var(--ui-border-inverted);
--stroke-color-default: var(--ui-border);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you need this one, let's add them all for stroke and fill. So you can add --stroke-color-muted, --fill-color-muted and --fill-color-accented I guess.

--stroke-color-muted: var(--ui-border-muted);
--stroke-color-accented: var(--ui-border-accented);
--stroke-color-inverted: var(--ui-border-inverted);
--fill-color-default: var(--ui-border);
--fill-color-muted: var(--ui-border-muted);
--fill-color-accented: var(--ui-border-accented);
--fill-color-inverted: var(--ui-border-inverted);
}
```
Expand Down
30 changes: 30 additions & 0 deletions docs/content/3.components/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ props:
---
::

### Variant

Use the `variant` prop to change the style of the Progress. Defaults to `linear`.

::component-code
---
external:
- modelValue
props:
modelValue: 50
variant: linear
---
::

### Status

Use the `status` prop to display the current Progress value above the bar.
Expand All @@ -71,6 +85,22 @@ props:
---
::

### Status Position

Use the `status-position` prop to define where the status text is displayed. Defaults to `outside`.

::component-code
---
external:
- modelValue
props:
modelValue: 50
variant: circular
status: true
statusPosition: 'inside'
---
::

### Indeterminate

When no `v-model` is set or the value is `null`, the Progress becomes _indeterminate_. The progress bar is animated as a `carousel`, but you can change it using the [`animation`](#animation) prop.
Expand Down
66 changes: 66 additions & 0 deletions playground/app/pages/components/progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,78 @@ onMounted(() => {
:max="max"
status
inverted
color="warning"
class="w-48 justify-start"
/>
</div>

<div class="h-48 flex items-center gap-8">
<UProgress v-for="size in sizes" :key="size" v-model="value1" orientation="vertical" :size="size" />
</div>

<div class="flex items-center gap-4">
<UProgress
v-model="value2"
variant="circular"
color="warning"
size="2xs"
status
/>

<UProgress
v-model="value2"
variant="circular"
size="xs"
color="neutral"
/>

<UProgress
v-model="value2"
variant="circular"
color="warning"
size="sm"
status
/>

<UProgress
v-model="value2"
variant="circular"
color="warning"
size="md"
status
/>

<UProgress
v-model="value2"
variant="circular"
color="warning"
size="lg"
status
/>

<UProgress
v-model="value2"
variant="circular"
color="warning"
status-position="inside"
size="xl"
status
/>

<UProgress
v-model="value2"
color="error"
status
size="2xl"
variant="circular"
>
<template #status="{ percent }">
<div class="flex flex-col items-center">
<span class="font-bold">{{ percent }}%</span>
<span class="text-sm text-gray-500">Loading...</span>
</div>
</template>
</UProgress>
</div>
</div>
</template>
71 changes: 66 additions & 5 deletions src/runtime/components/Progress.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' |
max?: number | Array<any>
/** Display the current progress value. */
status?: boolean
/**
* The position of the status text.
* @defaultValue 'outside'
*/
statusPosition?: 'inside' | 'outside'
/** Whether the progress is visually inverted. */
inverted?: boolean
/**
Expand All @@ -37,6 +42,11 @@ export interface ProgressProps extends Pick<ProgressRootProps, 'getValueLabel' |
* @defaultValue 'carousel'
*/
animation?: Progress['variants']['animation']
/**
* The variants of the progress bar.
* @defaultValue 'linear'
*/
variant?: Progress['variants']['variant']
class?: any
ui?: Progress['slots']
}
Expand All @@ -62,7 +72,9 @@ import { tv } from '../utils/tv'
const props = withDefaults(defineProps<ProgressProps>(), {
inverted: false,
modelValue: null,
orientation: 'horizontal'
orientation: 'horizontal',
variant: 'linear',
statusPosition: 'outside'
})
const emits = defineEmits<ProgressEmits>()
const slots = defineSlots<ProgressSlots>()
Expand All @@ -72,6 +84,9 @@ const appConfig = useAppConfig() as Progress['AppConfig']

const rootProps = useForwardPropsEmits(reactivePick(props, 'getValueLabel', 'modelValue'), emits)

const RADIUS = 40
const circumference = 2 * Math.PI * RADIUS

const isIndeterminate = computed(() => rootProps.value.modelValue === null)
const hasSteps = computed(() => Array.isArray(props.max))

Expand Down Expand Up @@ -99,7 +114,23 @@ const percent = computed(() => {
}
})

const dashOffset = computed(() =>
((percent.value || 0) / 100) * circumference
)
const trackPath = computed(() => {
const r = RADIUS
return `
M 50 50
m 0 -${r}
a ${r} ${r} 0 1 1 0 ${r * 2}
a ${r} ${r} 0 1 1 0 -${r * 2}
`
})

const indicatorStyle = computed(() => {
if (props.variant === 'circular') {
return
}
if (percent.value === undefined) {
return
}
Expand All @@ -123,7 +154,7 @@ const indicatorStyle = computed(() => {

const statusStyle = computed(() => {
return {
[props.orientation === 'vertical' ? 'height' : 'width']: percent.value ? `${percent.value}%` : 'fit-content'
[props.orientation === 'vertical' ? 'height' : 'width']: percent.value && (props.variant === 'linear') ? `${percent.value}%` : 'fit-content'
}
})

Expand Down Expand Up @@ -162,20 +193,50 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.progress ||
size: props.size,
color: props.color,
orientation: props.orientation,
inverted: props.inverted
inverted: props.inverted,
variant: props.variant,
statusPosition: props.statusPosition
}))
</script>

<template>
<Primitive :as="as" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div v-if="!isIndeterminate && (status || !!slots.status)" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<div v-if="!isIndeterminate && (variant !== 'circular' || statusPosition === 'outside') && (status || !!slots.status)" :class="ui.status({ class: props.ui?.status })" :style="statusStyle">
<slot name="status" :percent="percent">
{{ percent }}%
</slot>
</div>

<ProgressRoot v-bind="rootProps" :max="realMax" :class="ui.base({ class: props.ui?.base })" style="transform: translateZ(0)">
<ProgressIndicator :class="ui.indicator({ class: props.ui?.indicator })" :style="indicatorStyle" />
<ProgressIndicator v-if="variant === 'linear'" :class="ui.indicator({ class: props.ui?.indicator })" :style="indicatorStyle" />
<template v-else-if="variant === 'circular'">
<svg
:class="ui.base({ class: props.ui?.base })"
viewBox="0 0 100 100"
>
<path
:d="trackPath"
:class="ui.track({ class: props.ui?.track })"
/>
<ProgressIndicator as-child>
<path
v-if="percent && percent > 0"
:d="trackPath"
:class="ui.indicator({ class: props.ui?.indicator })"
:style="{
'stroke-linecap': 'round',
'stroke-dasharray': `${dashOffset}px, ${circumference}px`,
'stroke-dashoffset': '0px'
}"
/>
</ProgressIndicator>
</svg>
<div v-if="!isIndeterminate && variant === 'circular' && statusPosition === 'inside' && (status || !!slots.status)" :class="ui.status({ class: props.ui?.status })">
<slot name="status" :percent="percent">
{{ percent }}%
</slot>
</div>
</template>
</ProgressRoot>

<div v-if="hasSteps" :class="ui.steps({ class: props.ui?.steps })">
Expand Down
4 changes: 4 additions & 0 deletions src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,12 @@ export function getTemplates(options: ModuleOptions, uiConfig: Record<string, an
--outline-color-default: var(--ui-border);
--outline-color-inverted: var(--ui-border-inverted);
--stroke-default: var(--ui-border);
--stroke-muted: var(--ui-border-muted);
--stroke-accented: var(--ui-border-accented);
--stroke-inverted: var(--ui-border-inverted);
--fill-default: var(--ui-border);
--fill-muted: var(--ui-border-muted);
--fill-accented: var(--ui-border-accented);
--fill-inverted: var(--ui-border-inverted);
}
`
Expand Down
Loading