Skip to content

Commit

Permalink
fix(maz-ui): MazAnimateCounter - animation on mobile browsers
Browse files Browse the repository at this point in the history
  • Loading branch information
LouisMazel committed Dec 8, 2024
1 parent 4aeb5ca commit 984ec37
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function createLibraryComponentFile({
const COMPONENT_FILE_OUTPUT = resolve(_dirname, `../../../../lib/components/${filename}.vue`)

const componentTemplate = `<template>
<div class="m-${filenameKebab.split('-')[1]}">${filename}</div>
<div class="m-${filenameKebab.split('-')[1]} m-reset-css">${filename}</div>
</template>
`

Expand Down
19 changes: 17 additions & 2 deletions packages/docs/docs/components/maz-animated-counter.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ description: MazAnimatedCounter is a standalone component that allows you to ani

## Basic usage

<ComponentDemo>
<MazAnimatedCounter :count="4000" />
<ComponentDemo expanded>
<MazAnimatedCounter :count="count" />

<template #code>

Expand All @@ -23,12 +23,27 @@ description: MazAnimatedCounter is a standalone component that allows you to ani
<script lang="ts" setup>
import MazAnimatedCounter from 'maz-ui/components/MazAnimatedCounter'
const count = ref(Math.floor(Math.random() * 99999))
setInterval(() => {
count.value = Math.floor(Math.random() * 99999)
}, 3000)
</script>
```

</template>
</ComponentDemo>

<script setup lang="ts">
import { ref } from 'vue'
const count = ref(Math.floor(Math.random() * 99999))

setInterval(() => {
count.value = Math.floor(Math.random() * 99999)
}, 3000)
</script>

## duration

You can set the duration of the animation with the `duration` prop. The default value is `1000` ms.
Expand Down
112 changes: 38 additions & 74 deletions packages/lib/components/MazAnimatedCounter.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, ref, useSlots, watch } from 'vue'
import { ref, watch } from 'vue'
const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -33,97 +33,61 @@ const props = withDefaults(
delay: 100,
},
)
const slots = useSlots()
const hasPrefix = computed(() => !!props.prefix || !!slots.prefix)
const hasSuffix = computed(() => !!props.suffix || !!slots.suffix)
const currentCount = ref(0)
const isAnimated = ref(true)
function animate(start: number, end: number, duration: number, delay: number) {
currentCount.value = start
setTimeout(() => {
const startTime = performance.now()
const updateCount = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
const easeOutQuad = (t: number) => t * (2 - t)
currentCount.value = Math.round(
start + (end - start) * easeOutQuad(progress),
)
if (progress < 1) {
requestAnimationFrame(updateCount)
}
}
requestAnimationFrame(updateCount)
}, delay)
}
watch(
() => props.count,
(count, oldCount) => {
if (count === oldCount)
(newCount, oldCount) => {
if (newCount === oldCount) {
return
isAnimated.value = false
setTimeout(() => {
isAnimated.value = true
}, props.delay)
}
const startValue = oldCount ?? 0
animate(startValue, newCount, props.duration, props.delay)
},
{ immediate: true },
)
const durationInMs = computed(() => `${props.duration}ms`)
</script>

<template>
<span
class="m-animated-counter m-reset-css"
:class="{
'--animated': isAnimated,
'--prefixed': hasPrefix,
'--suffixed': hasSuffix,
}"
:style="{
'--count': count,
'--animation-duration': durationInMs,
}"
>
<span class="m-animated-counter m-reset-css">
<span class="maz-sr-only">
<slot name="prefix">{{ prefix }}</slot>
{{ count }}
<slot name="suffix">{{ suffix }}</slot>
<slot name="prefix">{{ prefix }}</slot>{{ count }}<slot name="suffix">{{ suffix }}</slot>
</span>

<span v-if="hasPrefix || hasSuffix" class="m-animated-counter__fix">
<!-- @slot Prefix slot - Add a prefix next to the number (e.g: "$") -->
<slot name="prefix">{{ prefix }}</slot>
<!-- @slot Suffix slot - Add a suffix next to the number (e.g: "%") -->
<slot name="suffix">{{ suffix }}</slot>
</span>
<!-- Keep this on one line to avoid spacing issues (space between elements) -->
<slot name="prefix">{{ prefix }}</slot>{{ currentCount }}<slot name="suffix">{{ suffix }}</slot>
</span>
</template>

<style lang="postcss" scoped>
.m-animated-counter {
.m-animated-counter {
@apply maz-whitespace-nowrap maz-tabular-nums;
&.--animated {
animation: counter var(--animation-duration) ease-out forwards;
counter-set: count var(--count-end);
}
&.--prefixed::after {
content: counter(count);
}
&.--suffixed::before {
content: counter(count);
}
&:not(.--prefixed, .--suffixed)::before {
content: counter(count);
}
}
@property --count {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
@property --count-end {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
@keyframes counter {
from {
--count-end: 0;
}
to {
--count-end: var(--count);
}
}
</style>
49 changes: 49 additions & 0 deletions packages/lib/tests/specs/components/MazAnimatedCounter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import MazAnimatedCounter from '@components/MazAnimatedCounter.vue'
import { shallowMount } from '@vue/test-utils'

describe('mazAnimatedCounter', () => {
it('renders initial count with prefix and suffix', async () => {
const wrapper = shallowMount(MazAnimatedCounter, {
props: { count: 10, prefix: '$' },
})

expect(wrapper.find('.maz-sr-only').text()).toBe('$10')
expect(wrapper.text()).toBe('$10$0')

await new Promise(resolve => setTimeout(resolve, 100))
expect(wrapper.text()).toBe('$10$0')

Check failure on line 14 in packages/lib/tests/specs/components/MazAnimatedCounter.spec.ts

View workflow job for this annotation

GitHub Actions / coverage

tests/specs/components/MazAnimatedCounter.spec.ts > mazAnimatedCounter > renders initial count with prefix and suffix

AssertionError: expected '$10$-20' to be '$10$0' // Object.is equality Expected: "$10$0" Received: "$10$-20" ❯ tests/specs/components/MazAnimatedCounter.spec.ts:14:28
})

it('updates count and triggers animation', async () => {
const wrapper = shallowMount(MazAnimatedCounter, {
props: { count: 10, suffix: '%' },
})

expect(wrapper.find('.maz-sr-only').text()).toBe('10%')
expect(wrapper.text()).toBe('10%0%')

await new Promise(resolve => setTimeout(resolve, 100))
expect(wrapper.text()).toBe('10%0%')

Check failure on line 26 in packages/lib/tests/specs/components/MazAnimatedCounter.spec.ts

View workflow job for this annotation

GitHub Actions / coverage

tests/specs/components/MazAnimatedCounter.spec.ts > mazAnimatedCounter > updates count and triggers animation

AssertionError: expected '10%-21%' to be '10%0%' // Object.is equality Expected: "10%0%" Received: "10%-21%" ❯ tests/specs/components/MazAnimatedCounter.spec.ts:26:28

await wrapper.setProps({ count: 20, suffix: '%' })
expect(wrapper.find('.maz-sr-only').text()).toBe('20%')
expect(wrapper.text()).toBe('20%10%')

// await new Promise(resolve => setTimeout(resolve, 100))
// expect(wrapper.text()).toBe('20%-52100%')
})

it('respects delay prop', async () => {
const wrapper = shallowMount(MazAnimatedCounter, {
props: { count: 10, delay: 200 },
})

expect(wrapper.text()).toBe('100')

await new Promise(resolve => setTimeout(resolve, 100))
expect(wrapper.text()).toBe('100')

// await new Promise(resolve => setTimeout(resolve, 200))
// expect(wrapper.html()).toBe('10')
})
})
46 changes: 0 additions & 46 deletions packages/lib/tests/specs/components/maz-animated-counter.spec.ts

This file was deleted.

0 comments on commit 984ec37

Please sign in to comment.