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: Add new component CountTo #1815

Merged
merged 8 commits into from
Nov 21, 2024
Merged
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
2 changes: 1 addition & 1 deletion packages/varlet-shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"build": "tsup src/index.ts --format esm,cjs --out-dir=lib --dts --clean"
},
"dependencies": {
"rattail": "1.0.0"
"rattail": "1.0.14"
},
"devDependencies": {
"@types/node": "^18.7.18",
Expand Down
2 changes: 1 addition & 1 deletion packages/varlet-shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './is.js'
export * from './is'
export * from 'rattail'
66 changes: 66 additions & 0 deletions packages/varlet-ui/src/count-to/CountTo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<template>
<div :class="n()">
<slot :value="value"> {{ value }} </slot>
</div>
</template>

<script lang="ts">
import { defineComponent, computed, onMounted, watch } from 'vue'
import { props } from './props'
import { createNamespace } from '../utils/components'
import { useMotion } from '@varlet/use'
import { call, floor, toNumber } from '@varlet/shared'

const { name, n } = createNamespace('count-to')

export default defineComponent({
name,
props,
setup(props) {
const {
value: _value,
reset: _reset,
// expose
start,
// expose
pause,
} = useMotion({
from: () => toNumber(props.from),
to: () => toNumber(props.to),
duration: () => toNumber(props.duration),
timingFunction: props.timingFunction,
onFinished() {
call(props.onEnd)
},
})

const value = computed(() => floor(_value.value, toNumber(props.precision)))

watch(() => [props.from, props.to, props.duration], reset)
onMounted(reset)

// expose
function reset() {
_reset()
if (props.autoStart) {
start()
}
}

return {
value,
n,
start,
pause,
reset,
toNumber,
floor,
}
},
})
</script>

<style lang="less">
@import '../styles/common';
@import './countTo';
</style>
140 changes: 140 additions & 0 deletions packages/varlet-ui/src/count-to/__tests__/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import CountTo from '..'
import { createApp } from 'vue'
import { expect, test, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { delay } from '../../utils/test'

test('test count-to plugin', () => {
const app = createApp({}).use(CountTo)
expect(app.component(CountTo.name)).toBeTruthy()
})

const DURATION = 100

test('test count-to basic animation', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
},
})

expect(wrapper.text()).toBe('0')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('100')
wrapper.unmount()
})

test('test count-to props change', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
},
})

expect(wrapper.text()).toBe('0')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('100')
await wrapper.setProps({ from: -200, to: 200 })
expect(wrapper.text()).toBe('-200')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('200')
wrapper.unmount()
})

test('test count-to slot', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
},
slots: {
default: ({ value }) => value * 2,
},
})

expect(wrapper.text()).toBe('0')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('200')
wrapper.unmount()
})

test('test count-to manual control', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
autoStart: false,
},
})

expect(wrapper.text()).toBe('0')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('0')
await wrapper.vm.start()
await delay(DURATION + 20)
expect(wrapper.text()).toBe('100')
await wrapper.vm.reset()
expect(wrapper.text()).toBe('0')
await wrapper.vm.pause()
await delay(DURATION + 20)
expect(wrapper.text()).toBe('0')
await wrapper.vm.start()
await delay(DURATION + 20)
expect(wrapper.text()).toBe('100')

wrapper.unmount()
})

test('test count-to precision', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 33.333,
duration: DURATION,
precision: 2,
},
})

expect(wrapper.text()).toBe('0')
await delay(DURATION + 20)
expect(wrapper.text()).toBe('33.33')
wrapper.unmount()
})

test('test count-to timing function', async () => {
const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
timingFunction: () => 1,
},
})

await delay(20)
expect(wrapper.text()).toBe('100')
wrapper.unmount()
})

test('test count-to end event', async () => {
const onEnd = vi.fn()

const wrapper = mount(CountTo, {
props: {
from: 0,
to: 100,
duration: DURATION,
onEnd,
},
})

await delay(DURATION + 20)
expect(onEnd).toHaveBeenCalled()
wrapper.unmount()
})
9 changes: 9 additions & 0 deletions packages/varlet-ui/src/count-to/countTo.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:root {
--count-to-text-color: var(--color-text);
--count-to-text-font-size: var(--font-size-lg);
}

.var-count-to {
color: var(--count-to-text-color);
font-size: var(--count-to-text-font-size);
}
118 changes: 118 additions & 0 deletions packages/varlet-ui/src/count-to/docs/en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# CountTo

### Introduction

Display a number animation transitioning from a start value to a target value.

### Basic Usage

The `from` and `to` attributes specify the start and target values.

```html
<template>
<var-count-to :from="0" :to="123456" />
</template>
```

### Precision

Controlling numerical precision with `precision`

```html
<template>
<var-count-to :from="0" :to="123456" />
</template>
```

### Custom Duration

The `duration` attribute specifies the animation duration in milliseconds.

```html
<template>
<var-count-to :from="0" :to="123456" :duration="100000" />
</template>
```

### Custom Styles

```html
<template>
<var-count-to :from="0" :to="123456">
<template #default="{ value }">
{{ value.toLocaleString() }}
</template>
</var-count-to>
</template>
```

### Custom Animation Curve

The `timing-function` attribute specifies the animation curve.

```html
<template>
<var-count-to :from="0" :to="123456" :timing-function="(v) => 1 - Math.pow(1 - v, 3)" />
</template>
```

### Manual Control

You can call the `start`, `pause`, and `reset` methods to manually control the start, pause, and reset of the animation.

```html
<script setup>
import { ref } from 'vue'

const countTo = ref()
</script>

<template>
<var-count-to ref="countTo" :from="0" :to="123456" :auto-start="false" />
<var-button @click="countToRef.start()">Start</var-button>
<var-button @click="countToRef.pause()">Pause</var-button>
<var-button @click="countToRef.reset()">Reset</var-button>
</template>
```

## API

### Props

| Prop | Description | Type | Default |
| ------------ | ---------------------------------------- | ------------------ | ------- |
| `from` | Start value | _number \| string_ | `0` |
| `to` | Target value | _number \| string_ | `0` |
| `duration` | Animation duration (ms) | _number \| string_ | `2000` |
| `precision` | Precision (ms) | _number \| string_ | `2` |
| `auto-start` | Whether to start animation automatically | _boolean_ | `true` |
| `timing-function` | Animation curve | _(v: number) => number_ | `v => v` |

### Events

| Event | Description | Arguments |
| ---------- | ----------------------------- | ------------------- |
| `end` | Triggered when animation ends | `-` |

### Slots

| Name | Description | SlotProps |
| --------- | -------------- | ------------------------ |
| `default` | Custom content | `value: number` |

### Methods

| Method | Description | Arguments | Return |
| ------- | ------------------------------------------------------------------------------------ | --------- | ------ |
| `start` | Start the animation | `-` | `-` |
| `pause` | Pause the animation | `-` | `-` |
| `reset` | Reset the animation. If `auto-start` is `true`, the animation will restart automatically | `-` | `-` |

### Style Variables

Here are the CSS variables used by the component. Styles can be customized using [StyleProvider](#/en-US/style-provider).

| Variable | Default |
| --- | --- |
| `--count-to-text-color` | `var(--color-text)` |
| `--count-to-text-font-size` | `var(--font-size-lg)` |
Loading
Loading