Skip to content
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

feat(web-core): circle progress bar component #8402

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[
prop('value').num().preset(75).required().widget(),
prop('max-value').num().default(100).preset(100).widget(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

The prop is required, plus there is no default value (only preset is enough):

Suggested change
prop('max-value').num().default(100).preset(100).widget(),
prop('max-value').num().required().preset(100).widget(),

prop('accent').required().enum('info', 'success', 'warning', 'danger').preset('info').widget(),
prop('size').required().enum('extra-small', 'small', 'medium', 'large').preset('large').widget(),
]"
:presets
>
<UiCircleProgressBar v-bind="properties" />
</ComponentStory>
</template>

<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { prop } from '@/libs/story/story-param'
import UiCircleProgressBar from '@core/components/ui/circle-progress-bar/UiCircleProgressBar.vue'

const presets = {
'Half of 500': {
props: {
'max-value': 500,
value: 250,
},
},
'75% of 300': {
props: {
'max-value': 300,
value: 225,
},
},
}
Comment on lines +21 to +34
Copy link
Collaborator

Choose a reason for hiding this comment

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

Be careful to use all the required props (accent and size are missing)

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!-- v3 -->
<template>
<div class="progress-circle-container">
Copy link
Collaborator

Choose a reason for hiding this comment

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

The CSS class should match the component's name and the guidelines

<svg
:width="circleSize"
:height="circleSize"
:viewBox="`0 0 ${circleSize} ${circleSize}`"
xmlns="http://www.w3.org/2000/svg"
class="progress-circle"
>
<circle
:r="radius"
:cx="circleSize / 2"
:cy="circleSize / 2"
fill="transparent"
class="progress-circle-background"
/>
<circle
:r="radius"
:cx="circleSize / 2"
:cy="circleSize / 2"
fill="transparent"
class="progress-circle-foreground progress-circle-fill"
/>
</svg>
<div v-if="size !== 'extra-small'" class="progress-circle-overlay">
<VtsIcon v-if="isComplete" :icon="icon" class="progress-circle-icon" :accent="iconAccent" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
<VtsIcon v-if="isComplete" :icon="icon" class="progress-circle-icon" :accent="iconAccent" />
<VtsIcon v-if="isComplete" :icon class="progress-circle-icon" :accent="iconAccent" />

<span v-else class="progress-circle-text" :class="fontClass">{{ percentValue }}</span>
</div>
</div>
</template>

<script setup lang="ts">
import VtsIcon from '@core/components/icon/VtsIcon.vue'
import { faCheck, faExclamation } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'

const { accent, size, value, maxValue } = defineProps<{
accent: ProgressCircleAccent
size: ProgressCircleSize
value: number
maxValue: number
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if maxValue could be optional, and in this case, compute the completion percentage based on a default value?
I mean, in most cases, we would want to display the completion based on 100%, so maybe this could be the default value?

}>()

type ProgressCircleAccent = 'info' | 'success' | 'warning' | 'danger'
type ProgressCircleSize = 'extra-small' | 'small' | 'medium' | 'large'

const circleSizeMap = {
'extra-small': 16,
small: 40,
medium: 64,
large: 164,
}

const iconSizeMap = {
'extra-small': '',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can use undefined here? The same applies on line 70.

small: '1.6rem',
medium: '3.2rem',
large: '4.8rem',
}

const strokeWidthMap = {
'extra-small': 2,
small: 4,
medium: 6,
large: 16,
}

const fontClasses = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

To keep consistency?

Suggested change
const fontClasses = {
const fontClassesMap = {

'extra-small': '',
small: 'typo-body-bold-small',
medium: 'typo-h5',
large: 'typo-h3',
}

const circleSize = computed(() => circleSizeMap[size])
const fontClass = computed(() => fontClasses[size])
const iconSize = computed(() => iconSizeMap[size])
const strokeWidth = computed(() => strokeWidthMap[size])

const radius = computed(() => (circleSize.value - strokeWidth.value) / 2)
const circumference = computed(() => 2 * Math.PI * radius.value)

const isComplete = computed(() => value >= maxValue)

const strokeColor = computed(() =>
size === 'extra-small'
? `var(--color-${accent}-item-base)`
: isComplete.value && (accent === 'info' || accent === 'success')
? 'var(--color-success-item-base)'
: `var(--color-${accent}-item-base)`
)
Comment on lines +86 to +92
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we could have better readability if we extract a config object, like above:

Suggested change
const strokeColor = computed(() =>
size === 'extra-small'
? `var(--color-${accent}-item-base)`
: isComplete.value && (accent === 'info' || accent === 'success')
? 'var(--color-success-item-base)'
: `var(--color-${accent}-item-base)`
)
const strokeColorMap = {
info: 'var(--color-info-item-base)',
success: 'var(--color-success-item-base)',
warning: 'var(--color-warning-item-base)',
danger: 'var(--color-danger-item-base)',
}
const strokeColor = computed(() => {
if (isComplete.value && (accent === 'info' || accent === 'success')) {
return strokeColorMap.success
}
return strokeColorMap[accent]
})


const backgroundStrokeColor = computed(() => `var(--color-${accent}-background-selected)`)

const iconAccent = computed(() =>
isComplete.value && (accent === 'info' || accent === 'success') ? 'success' : accent
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since the condition isComplete.value && (accent === 'info' || accent === 'success') is used twice, maybe it could be refactored into a computed property:

const isCompleteWithSuccess = computed(() => isComplete.value && ['info', 'success'].includes(accent))

)
const valuePercent = computed(() => Math.round((value / maxValue) * 100))

const dashOffset = computed(() => {
if (valuePercent.value > 100) return
return circumference.value * (1 - valuePercent.value / 100)
})

const percentValue = computed(() => `${valuePercent.value}%`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can use n from useI18n(), it will handle space between the value and %, which may differ based on the locale:

Suggested change
const percentValue = computed(() => `${valuePercent.value}%`)
const percentValue = computed(() => n(valuePercent.value / 100, 'percent'))


const icon = computed(() => (accent === 'warning' || accent === 'danger' ? faExclamation : faCheck))
</script>

<style lang="postcss" scoped>
.progress-circle-container {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}

.progress-circle-background {
stroke: v-bind(backgroundStrokeColor);
stroke-width: v-bind(strokeWidth);
}

.progress-circle-foreground {
stroke-width: v-bind(strokeWidth);
stroke-linecap: butt;
transition: stroke-dashoffset 0.3s ease;
transform: rotate(-90deg);
transform-origin: center;
}

.progress-circle-fill {
stroke: v-bind(strokeColor);
stroke-dasharray: v-bind(circumference);
stroke-dashoffset: v-bind(dashOffset);
}

.progress-circle-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.progress-circle-text {
color: v-bind(strokeColor);
}

.progress-circle-icon {
font-size: v-bind(iconSize);
}
</style>
Comment on lines +111 to +152
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remember to use our guidelines for CSS

Loading