diff --git a/packages/vant/src/rolling-text-group/RollingTextGroup.tsx b/packages/vant/src/rolling-text-group/RollingTextGroup.tsx new file mode 100644 index 00000000000..1818c00228b --- /dev/null +++ b/packages/vant/src/rolling-text-group/RollingTextGroup.tsx @@ -0,0 +1,70 @@ +import { + defineComponent, + type InjectionKey, + type ExtractPropTypes, + PropType, +} from 'vue'; + +// Utils +import { truthProp, createNamespace, makeNumberProp } from '../utils'; + +// Composables +import { useChildren } from '@vant/use'; +import { useExpose } from '../composables/use-expose'; +import { RollingTextGroupExpose } from './types'; +import { RollingTextDirection, RollingTextStopOrder } from '../rolling-text'; + +const [name, bem] = createNamespace('rolling-text-group'); + +export const rollingTextGroupProps = { + startNum: makeNumberProp(0), + duration: Number, + autoStart: truthProp, + direction: String as PropType, + stopOrder: String as PropType, + height: Number, +}; + +export type RollingTextGroupProps = ExtractPropTypes< + typeof rollingTextGroupProps +>; +export type RollingTextGroupProvide = { + props: RollingTextGroupProps; +}; +export const ROLLING_TEXT_KEY: InjectionKey = + Symbol(name); + +export default defineComponent({ + name, + + props: rollingTextGroupProps, + + setup(props, { slots }) { + const { children, linkChildren } = useChildren(ROLLING_TEXT_KEY); + + linkChildren({ + props, + }); + + const start = () => { + children.map((ins) => { + ins.start(); + }); + }; + + const reset = () => { + children.map((ins) => { + ins.reset(); + }); + }; + + useExpose({ + start, + reset, + }); + + if (slots.default) { + return () =>
{slots.default!()}
; + } + }, +}); diff --git a/packages/vant/src/rolling-text-group/index.ts b/packages/vant/src/rolling-text-group/index.ts new file mode 100644 index 00000000000..837f7474fe5 --- /dev/null +++ b/packages/vant/src/rolling-text-group/index.ts @@ -0,0 +1,15 @@ +import { withInstall } from '../utils'; +import _RollingTextGroup from './RollingTextGroup'; + +export const RollingTextGroup = withInstall(_RollingTextGroup); +export default RollingTextGroup; + +export type { + RollingTextGroupInstance, +} from './types'; + +declare module 'vue' { + export interface GlobalComponents { + RollingTextGroup: typeof RollingTextGroup; + } +} diff --git a/packages/vant/src/rolling-text-group/test/__snapshots__/index.spec.tsx.snap b/packages/vant/src/rolling-text-group/test/__snapshots__/index.spec.tsx.snap new file mode 100644 index 00000000000..d7a5328b0f6 --- /dev/null +++ b/packages/vant/src/rolling-text-group/test/__snapshots__/index.spec.tsx.snap @@ -0,0 +1,537 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should render default slot correctly 1`] = ` +
+
+
+
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+
+
+ + . + +
+
+
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+ 9 +
+
+ 8 +
+
+ 7 +
+
+ 6 +
+
+ 5 +
+
+ 4 +
+
+ 3 +
+
+ 2 +
+
+ 1 +
+
+ 0 +
+
+
+
+
+`; diff --git a/packages/vant/src/rolling-text-group/test/index.spec.tsx b/packages/vant/src/rolling-text-group/test/index.spec.tsx new file mode 100644 index 00000000000..f0844d4e4cb --- /dev/null +++ b/packages/vant/src/rolling-text-group/test/index.spec.tsx @@ -0,0 +1,154 @@ +import { nextTick } from 'vue'; +import { mount } from '../../../test'; +import { RollingTextGroup } from '../index'; +import { RollingText } from '../../rolling-text'; +import { RollingTextGroupInstance } from '../types'; + +const itemWrapperClass = '.van-rolling-text-item__box'; +const animationClass = 'van-rolling-text-item__box--animate'; + +test('should render default slot correctly', async () => { + const wrapper = mount({ + render() { + return ( + + + . + + + ); + }, + }); + await nextTick(); + expect(wrapper.html()).toMatchSnapshot(); +}); + +test('should set start-num for all rollingTexts unless already specified', async () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + await nextTick(); + const boxes = wrapper.findAll('.van-rolling-text-item__box'); + + const firstRollingText = boxes[0] + .findAll('.van-rolling-text-item__item')[0] + .text(); + expect(firstRollingText).toEqual('1'); + + const secondRollingText = boxes[1] + .findAll('.van-rolling-text-item__item')[0] + .text(); + expect(secondRollingText).toEqual('2'); +}); + +test('should set duration for all rollingTexts unless already specified', async () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + await nextTick(); + const rollingTexts = wrapper.findAll('.van-rolling-text-item'); + + expect(rollingTexts[0].style.getPropertyValue('--van-duration')).toEqual( + '1s', + ); + expect(rollingTexts[1].style.getPropertyValue('--van-duration')).toEqual( + '0.5s', + ); +}); + +test('should set direction for all rollingTexts unless already specified', async () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + await nextTick(); + const rollingTexts = wrapper.findAll('.van-rolling-text-item'); + + expect(rollingTexts[0].classes()).toContain('van-rolling-text-item--up'); + expect(rollingTexts[1].classes()).toContain('van-rolling-text-item--down'); +}); + +test('should set stopOrder for all rollingTexts unless already specified', async () => { + const wrapper = mount({ + render() { + return ( + + + + + + ); + }, + }); + await nextTick(); + const rollingTexts = wrapper.findAll('.van-rolling-text-item'); + + expect(rollingTexts[0].style.getPropertyValue('--van-delay')).toEqual('0s'); + expect(rollingTexts[1].style.getPropertyValue('--van-delay')).toEqual('0.2s'); + expect(rollingTexts[2].style.getPropertyValue('--van-delay')).toEqual('0s'); +}); + +test('should set height for all rollingTexts unless already specified', async () => { + const wrapper = mount({ + render() { + return ( + + + + + ); + }, + }); + await nextTick(); + const boxes = wrapper.findAll('.van-rolling-text-item__box'); + + const firstRollingText = boxes[0].findAll('.van-rolling-text-item__item')[0]; + expect(firstRollingText.style.lineHeight).toEqual('50px'); + + const secondRollingText = boxes[1].findAll('.van-rolling-text-item__item')[0]; + expect(secondRollingText.style.lineHeight).toEqual('100px'); +}); + +test('should control the animation of all rollingTexts', async () => { + const wrapper = mount(RollingTextGroup, { + props: { + autoStart: false, + }, + slots: { + default: () => ( + <> + + + + ), + }, + }); + const instance = wrapper.vm; + instance.start(); + await nextTick(); + expect(wrapper.find(itemWrapperClass).classes()).toContain(animationClass); + + instance.reset(); + await nextTick(); + expect(wrapper.find(itemWrapperClass).classes()).not.contain(animationClass); +}); diff --git a/packages/vant/src/rolling-text-group/types.ts b/packages/vant/src/rolling-text-group/types.ts new file mode 100644 index 00000000000..5f2d733668c --- /dev/null +++ b/packages/vant/src/rolling-text-group/types.ts @@ -0,0 +1,12 @@ +import { RollingTextGroupProps } from './RollingTextGroup'; +import type { ComponentPublicInstance } from 'vue'; + +export type RollingTextGroupExpose = { + start: () => void; + reset: () => void; +}; + +export type RollingTextGroupInstance = ComponentPublicInstance< + RollingTextGroupProps, + RollingTextGroupExpose +>; diff --git a/packages/vant/src/rolling-text/README.md b/packages/vant/src/rolling-text/README.md index c6a8075a77b..069067ab301 100644 --- a/packages/vant/src/rolling-text/README.md +++ b/packages/vant/src/rolling-text/README.md @@ -10,10 +10,11 @@ Register component globally via `app.use`, refer to [Component Registration](#/e ```js import { createApp } from 'vue'; -import { RollingText } from 'vant'; +import { RollingText, RollingTextGroup } from 'vant'; const app = createApp(); app.use(RollingText); +app.use(RollingTextGroup); ``` ## Usage @@ -93,6 +94,21 @@ The RollingText component provides some CSS variables that you can override to c } ``` +### RollingText Group + +You can use RollingTextGroup to control the animation of all RollingTexts, and add other element. Please upgrade vant to >= v4.9.0 before using this component. + +```html + + + + + . + + + +``` + ### Manual Control After obtaining the component instance through `ref`, you can call the `start` and `reset` methods. The `start` method is used to start the animation, and the `reset` method is used to reset the animation. @@ -129,7 +145,7 @@ export default { ## API -### Props +### RollingText Props | Attribute | Description | Type | Default | | --- | --- | --- | --- | @@ -142,7 +158,18 @@ export default { | stop-order | Order of stopping the animation of each digit, with `ltr` and `rtl` as the values | _string_ | `ltr` | | height | Height of digit, `px` as unit | _number_ | `40` | -### Methods +### RollingTextGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| start-num `v4.9.0` | Start number of all RollingTexts | _number_ | `0` | +| duration `v4.9.0` | Duration of all RollingTexts, in seconds | _number_ | `2` | +| direction `v4.9.0` | Rolling direction of all RollingTexts, with `down` and `up` as the values | _string_ | `down` | +| auto-start `v4.9.0` | Whether to start all RollingTexts | _boolean_ | `true` | +| stop-order `v4.9.0` | Order to stop all RollingTexts, with `ltr` and `rtl` as the values | _string_ | `ltr` | +| height `v4.9.0` | Height of all RollingTexts, `px` as unit | _number_ | `40` | + +### RollingText Methods Use [ref](https://vuejs.org/guide/essentials/template-refs.html) to get RollingText instance and call instance methods. @@ -151,6 +178,15 @@ Use [ref](https://vuejs.org/guide/essentials/template-refs.html) to get RollingT | start | Start the animation | - | - | | reset | Reset the animation | - | - | +### RollingTextGroup Methods + +Use [ref](https://vuejs.org/guide/essentials/template-refs.html) to get RollingTextGroup instance and call instance methods. + +| Name | Description | Attribute | Return value | +| -------------- | ------------------- | --------- | ------------ | +| start `v4.9.0` | Start the animation | - | - | +| reset `v4.9.0` | Reset the animation | - | - | + ### Types The component exports the following type definitions: @@ -161,18 +197,22 @@ import type { RollingTextInstance, RollingTextDirection, RollingTextStopOrder, + RollingTextGroupProps, + RollingTextGroupInstance, } from 'vant'; ``` -`RollingTextInstance` is the type of component instance: +`RollingTextInstance` and `RollingTextGroupInstance` is the type of component instance: ```ts import { ref } from 'vue'; -import type { RollingTextInstance } from 'vant'; +import type { RollingTextInstance, RollingTextGroupInstance } from 'vant'; const rollingTextRef = ref(); +const rollingTextGroupRef = ref(); rollingTextRef.value?.start(); +rollingTextGroupRef.value?.start(); ``` ## Theming diff --git a/packages/vant/src/rolling-text/README.zh-CN.md b/packages/vant/src/rolling-text/README.zh-CN.md index 6b20f4342fb..cc579c61d26 100644 --- a/packages/vant/src/rolling-text/README.zh-CN.md +++ b/packages/vant/src/rolling-text/README.zh-CN.md @@ -10,10 +10,11 @@ ```js import { createApp } from 'vue'; -import { RollingText } from 'vant'; +import { RollingText, RollingTextGroup } from 'vant'; const app = createApp(); app.use(RollingText); +app.use(RollingTextGroup); ``` ## 代码演示 @@ -93,6 +94,21 @@ RollingText 组件提供了一些 CSS 变量,你可以覆盖这些变量来自 } ``` +### 翻滚文本组 + +你可以使用翻滚文本组来控制多个翻滚文本的动画,并且可以加入自定义元素。请升级 vant 到 >= 4.9.0 版本来使用该组件。 + +```html + + + + + . + + + +``` + ### 手动控制 通过 ref 获取到组件实例后,你可以调用 `start`、`reset` 方法,`start` 方法用于开始动画,`reset` 方法用于重置动画。 @@ -129,7 +145,7 @@ export default { ## API -### Props +### RollingText Props | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | @@ -142,6 +158,17 @@ export default { | stop-order | 各个数位动画停止先后顺序,值为 `ltr` 和 `rtl` | _string_ | `ltr` | | height | 数字高度,单位为 `px` | _number_ | `40` | +### RollingTextGroup Props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| start-num `v4.9.0` | 所有翻滚文本的起始数值 | _number_ | `0` | +| duration `v4.9.0` | 所有翻滚文本的动画时长,单位为秒 | _number_ | `2` | +| direction `v4.9.0` | 所有翻滚文本的文本翻滚方向,值为 `down` 和 `up` | _string_ | `down` | +| auto-start `v4.9.0` | 是否自动开始所有翻滚文本动画 | _boolean_ | `true` | +| stop-order `v4.9.0` | 各个翻滚文本的动画停止先后顺序,值为 `ltr` 和 `rtl` | _string_ | `ltr` | +| height `v4.9.0` | 所有翻滚文本的数字高度,单位为 `px` | _number_ | `40` | + ### 方法 通过 ref 可以获取到 RollingText 实例并调用实例方法,详见[组件实例方法](#/zh-CN/advanced-usage#zu-jian-shi-li-fang-fa)。 @@ -151,6 +178,15 @@ export default { | start | 开始动画 | - | - | | reset | 重置动画 | - | - | +### RollingTextGroup 方法 + +通过 ref 可以获取到 RollingTextGroup 实例并调用实例方法。 + +| 方法名 | 说明 | 参数 | 返回值 | +| -------------- | -------- | ---- | ------ | +| start `v4.9.0` | 开始动画 | - | - | +| reset `v4.9.0` | 重置动画 | - | - | + ### 类型定义 组件导出以下类型定义: @@ -161,18 +197,22 @@ import type { RollingTextInstance, RollingTextDirection, RollingTextStopOrder, + RollingTextGroupProps, + RollingTextGroupInstance, } from 'vant'; ``` -`RollingTextInstance` 是组件实例的类型,用法如下: +`RollingTextInstance` 和 `RollingTextGroupInstance` 是组件实例的类型,用法如下: ```ts import { ref } from 'vue'; -import type { RollingTextInstance } from 'vant'; +import type { RollingTextInstance, RollingTextGroupInstance } from 'vant'; const rollingTextRef = ref(); +const rollingTextGroupRef = ref(); rollingTextRef.value?.start(); +rollingTextGroupRef.value?.start(); ``` ## 主题定制 diff --git a/packages/vant/src/rolling-text/RollingText.tsx b/packages/vant/src/rolling-text/RollingText.tsx index f308b7289fa..42700cd0da8 100644 --- a/packages/vant/src/rolling-text/RollingText.tsx +++ b/packages/vant/src/rolling-text/RollingText.tsx @@ -1,20 +1,23 @@ import { ref, - defineComponent, - computed, watch, + computed, + defineComponent, type ExtractPropTypes, + getCurrentInstance, } from 'vue'; // Utils -import { raf } from '@vant/use'; +import { raf, useParent } from '@vant/use'; import { - createNamespace, + padZero, + truthProp, makeArrayProp, - makeNumberProp, makeStringProp, - truthProp, - padZero, + makeNumberProp, + createNamespace, + isDef, + kebabCase, } from '../utils'; // Composables @@ -22,6 +25,10 @@ import { useExpose } from '../composables/use-expose'; // Components import RollingTextItem from './RollingTextItem'; +import { + ROLLING_TEXT_KEY, + RollingTextGroupProps, +} from '../rolling-text-group/RollingTextGroup'; // Types import { @@ -53,13 +60,42 @@ export default defineComponent({ props: rollingTextProps, setup(props) { + const instance = getCurrentInstance(); + const { parent, index } = useParent(ROLLING_TEXT_KEY); + + const getProp = ( + key: K, + ): RollingTextProps[K] => { + if ( + instance && + instance.vnode.props && + (key in instance.vnode.props || kebabCase(key) in instance.vnode.props) + ) { + return props[key]; + } + if ( + parent && + key in parent.props && + isDef(parent.props[key as keyof RollingTextGroupProps]) + ) { + return parent.props[ + key as keyof RollingTextGroupProps + ] as RollingTextProps[K]; + } + return props[key]; + }; + + const startNum = computed(() => { + return getProp('startNum'); + }); + const isCustomType = computed( () => Array.isArray(props.textList) && props.textList.length, ); const itemLength = computed(() => { if (isCustomType.value) return props.textList[0].length; - return `${Math.max(props.startNum, props.targetNum!)}`.length; + return `${Math.max(startNum.value, props.targetNum!)}`.length; }); const getTextArrByIdx = (idx: number) => { @@ -76,7 +112,7 @@ export default defineComponent({ }); const startNumArr = computed(() => - padZero(props.startNum, itemLength.value).split(''), + padZero(startNum.value, itemLength.value).split(''), ); const getFigureArr = (i: number) => { @@ -98,11 +134,20 @@ export default defineComponent({ }; const getDelay = (i: number, len: number) => { - if (props.stopOrder === 'ltr') return 0.2 * i; + if (parent) { + i = index.value; + len = parent.children.length; + } + const stopOrder = getProp('stopOrder'); + if (stopOrder === 'ltr') return 0.2 * i; return 0.2 * (len - 1 - i); }; - const rolling = ref(props.autoStart); + const autoStart = computed(() => { + return getProp('autoStart'); + }); + + const rolling = ref(autoStart.value); const start = () => { rolling.value = true; @@ -111,13 +156,13 @@ export default defineComponent({ const reset = () => { rolling.value = false; - if (props.autoStart) { + if (autoStart.value) { raf(() => start()); } }; watch( - () => props.autoStart, + () => autoStart.value, (value) => { if (value) { start(); @@ -137,10 +182,10 @@ export default defineComponent({ figureArr={ isCustomType.value ? getTextArrByIdx(i) : getFigureArr(i) } - duration={props.duration} - direction={props.direction} + duration={getProp('duration')} + direction={getProp('direction')} isStart={rolling.value} - height={props.height} + height={getProp('height')} delay={getDelay(i, itemLength.value)} /> ))} diff --git a/packages/vant/src/rolling-text/demo/index.vue b/packages/vant/src/rolling-text/demo/index.vue index fc08caa7583..82b7811721b 100644 --- a/packages/vant/src/rolling-text/demo/index.vue +++ b/packages/vant/src/rolling-text/demo/index.vue @@ -1,5 +1,6 @@