From acced75df601e1dd9c90a944526155cfdae6c0ae Mon Sep 17 00:00:00 2001 From: 07akioni <07akioni2@gmail.com> Date: Mon, 7 Oct 2024 22:19:38 +0800 Subject: [PATCH] feat(marquee): new component --- CHANGELOG.en-US.md | 1 + CHANGELOG.zh-CN.md | 1 + demo/routes/routes.js | 8 + demo/store/menu-options.js | 7 + src/alert/demos/enUS/index.demo-entry.md | 1 + src/alert/demos/enUS/marquee.demo.vue | 15 ++ src/alert/demos/zhCN/index.demo-entry.md | 1 + src/alert/demos/zhCN/marquee.demo.vue | 15 ++ src/components.ts | 1 + src/config-provider/src/internal-interface.ts | 2 + src/marquee/demos/enUS/auto-fill.demo.vue | 16 ++ src/marquee/demos/enUS/basic.demo.vue | 15 ++ src/marquee/demos/enUS/image.demo.vue | 16 ++ src/marquee/demos/enUS/index.demo-entry.md | 28 ++++ src/marquee/demos/zhCN/auto-fill.demo.vue | 16 ++ src/marquee/demos/zhCN/basic.demo.vue | 11 ++ src/marquee/demos/zhCN/image.demo.vue | 16 ++ src/marquee/demos/zhCN/index.demo-entry.md | 28 ++++ src/marquee/index.ts | 2 + src/marquee/src/Marquee.tsx | 148 ++++++++++++++++++ src/marquee/src/props.ts | 15 ++ src/marquee/src/public-types.ts | 1 + src/marquee/src/styles/index.cssr.ts | 40 +++++ src/marquee/styles/dark.ts | 11 ++ src/marquee/styles/index.ts | 3 + src/marquee/styles/light.ts | 17 ++ src/marquee/tests/Marquee.spec.ts | 8 + src/marquee/tests/server.spec.tsx | 20 +++ src/themes/dark.ts | 4 +- src/themes/light.ts | 4 +- 30 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 src/alert/demos/enUS/marquee.demo.vue create mode 100644 src/alert/demos/zhCN/marquee.demo.vue create mode 100644 src/marquee/demos/enUS/auto-fill.demo.vue create mode 100644 src/marquee/demos/enUS/basic.demo.vue create mode 100644 src/marquee/demos/enUS/image.demo.vue create mode 100644 src/marquee/demos/enUS/index.demo-entry.md create mode 100644 src/marquee/demos/zhCN/auto-fill.demo.vue create mode 100644 src/marquee/demos/zhCN/basic.demo.vue create mode 100644 src/marquee/demos/zhCN/image.demo.vue create mode 100644 src/marquee/demos/zhCN/index.demo-entry.md create mode 100644 src/marquee/index.ts create mode 100644 src/marquee/src/Marquee.tsx create mode 100644 src/marquee/src/props.ts create mode 100644 src/marquee/src/public-types.ts create mode 100644 src/marquee/src/styles/index.cssr.ts create mode 100644 src/marquee/styles/dark.ts create mode 100644 src/marquee/styles/index.ts create mode 100644 src/marquee/styles/light.ts create mode 100644 src/marquee/tests/Marquee.spec.ts create mode 100644 src/marquee/tests/server.spec.tsx diff --git a/CHANGELOG.en-US.md b/CHANGELOG.en-US.md index f70b1c32964..7c694ac867b 100644 --- a/CHANGELOG.en-US.md +++ b/CHANGELOG.en-US.md @@ -10,6 +10,7 @@ ### Features +- 🌟 Adds `n-marquee` component. - `n-image` adds `error` slot, closes [#5649](https://github.com/tusen-ai/naive-ui/issues/5649) - `n-date-picker` adds `date-format` prop. - `n-progress`'s `color` prop supports gradient config. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index fa3c1c17de0..06e42e84b73 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -10,6 +10,7 @@ ### Features +- 🌟 新增 `n-marquee` 组件 - `n-image` 新增 `error` 插槽,关闭 [#5649](https://github.com/tusen-ai/naive-ui/issues/5649) - `n-date-picker` 新增 `date-format` 属性 - `n-progress` 的 `color` 属性支持渐变色配置 diff --git a/demo/routes/routes.js b/demo/routes/routes.js index 7acdefdb580..dd8a77c1c88 100644 --- a/demo/routes/routes.js +++ b/demo/routes/routes.js @@ -590,6 +590,10 @@ export const enComponentRoutes = [ path: 'highlight', component: () => import('../../src/highlight/demos/enUS/index.demo-entry.md') + }, + { + path: 'marquee', + component: () => import('../../src/marquee/demos/enUS/index.demo-entry.md') } ] @@ -999,6 +1003,10 @@ export const zhComponentRoutes = [ path: 'highlight', component: () => import('../../src/highlight/demos/zhCN/index.demo-entry.md') + }, + { + path: 'marquee', + component: () => import('../../src/marquee/demos/zhCN/index.demo-entry.md') } ] diff --git a/demo/store/menu-options.js b/demo/store/menu-options.js index e480632d6ac..0c520fd51ca 100644 --- a/demo/store/menu-options.js +++ b/demo/store/menu-options.js @@ -663,6 +663,13 @@ export function createComponentMenuOptions({ lang, theme }) { enSuffix: true, path: '/drawer' }, + { + en: 'Marquee', + zh: '跑马灯', + enSuffix: true, + path: '/marquee', + isNew: true + }, { en: 'Message', zh: '信息', diff --git a/src/alert/demos/enUS/index.demo-entry.md b/src/alert/demos/enUS/index.demo-entry.md index 771fdc5a24b..27f6b62d557 100644 --- a/src/alert/demos/enUS/index.demo-entry.md +++ b/src/alert/demos/enUS/index.demo-entry.md @@ -10,6 +10,7 @@ bordered.vue closable.vue icon.vue no-icon.vue +marquee.vue ``` ## API diff --git a/src/alert/demos/enUS/marquee.demo.vue b/src/alert/demos/enUS/marquee.demo.vue new file mode 100644 index 00000000000..39a1d523598 --- /dev/null +++ b/src/alert/demos/enUS/marquee.demo.vue @@ -0,0 +1,15 @@ + +# Marquee + +You can use `n-marquee` to achieve marquee effect. + + + diff --git a/src/alert/demos/zhCN/index.demo-entry.md b/src/alert/demos/zhCN/index.demo-entry.md index 935b4a449b1..952938e4433 100644 --- a/src/alert/demos/zhCN/index.demo-entry.md +++ b/src/alert/demos/zhCN/index.demo-entry.md @@ -12,6 +12,7 @@ bordered.vue closable.vue icon.vue no-icon.vue +marquee.vue rtl-debug.vue empty-debug.vue ``` diff --git a/src/alert/demos/zhCN/marquee.demo.vue b/src/alert/demos/zhCN/marquee.demo.vue new file mode 100644 index 00000000000..5705913dafa --- /dev/null +++ b/src/alert/demos/zhCN/marquee.demo.vue @@ -0,0 +1,15 @@ + +# 跑马灯 + +你可以配合 `n-marquee` 实现轮播的效果。 + + + diff --git a/src/components.ts b/src/components.ts index 6f79adf7fc0..ebedc76139b 100644 --- a/src/components.ts +++ b/src/components.ts @@ -53,6 +53,7 @@ export * from './list' export * from './loading-bar' export * from './log' export * from './infinite-scroll' +export * from './marquee' export * from './menu' export * from './mention' export * from './message' diff --git a/src/config-provider/src/internal-interface.ts b/src/config-provider/src/internal-interface.ts index 1e3b871d985..92d19c4b58a 100644 --- a/src/config-provider/src/internal-interface.ts +++ b/src/config-provider/src/internal-interface.ts @@ -100,6 +100,7 @@ import type { RowTheme } from '../../legacy-grid/styles' import type { SplitTheme } from '../../split/styles' import type { FlexTheme } from '../../flex/styles' import type { FloatButtonGroupTheme } from '../../float-button-group/styles' +import type { MarqueeTheme } from '../../marquee/styles' import type { Katex } from './katex' import type { GlobalTheme, GlobalThemeOverrides } from './interface' @@ -190,6 +191,7 @@ export interface GlobalThemeWithoutCommon { Watermark?: WatermarkTheme Split?: SplitTheme Row?: RowTheme + Marquee?: MarqueeTheme // internal InternalSelectMenu?: InternalSelectMenuTheme InternalSelection?: InternalSelectionTheme diff --git a/src/marquee/demos/enUS/auto-fill.demo.vue b/src/marquee/demos/enUS/auto-fill.demo.vue new file mode 100644 index 00000000000..35e41d63b3f --- /dev/null +++ b/src/marquee/demos/enUS/auto-fill.demo.vue @@ -0,0 +1,16 @@ + +# Auto fill + +Use `auto-fill` prop to fill all the blank space that left. + + + diff --git a/src/marquee/demos/enUS/basic.demo.vue b/src/marquee/demos/enUS/basic.demo.vue new file mode 100644 index 00000000000..86e38dc2de9 --- /dev/null +++ b/src/marquee/demos/enUS/basic.demo.vue @@ -0,0 +1,15 @@ + +# Basic + +Put text into marquee: + + + diff --git a/src/marquee/demos/enUS/image.demo.vue b/src/marquee/demos/enUS/image.demo.vue new file mode 100644 index 00000000000..9d419ded1cf --- /dev/null +++ b/src/marquee/demos/enUS/image.demo.vue @@ -0,0 +1,16 @@ + +# Image + +You can put any content inside marquee. + + + diff --git a/src/marquee/demos/enUS/index.demo-entry.md b/src/marquee/demos/enUS/index.demo-entry.md new file mode 100644 index 00000000000..8e4f127a526 --- /dev/null +++ b/src/marquee/demos/enUS/index.demo-entry.md @@ -0,0 +1,28 @@ +# Marquee + +A trivia: There's a deprecated HTML Element called `marquee`. + +Available since `NEXT_VERSION`. + +## Demos + +```demo +basic.vue +image.vue +auto-fill.vue +``` + +## API + +### Marquee Props + +| Name | Type | Default | Description | Version | +| --- | --- | --- | --- | --- | +| auto-fill | `boolean` | `false` | Whether to fill the blank of the container using its content repeatly. | NEXT_VERSION | +| speed | `number` | `48` | The speed calculated as pixels/second. | NEXT_VERSION | + +### Marquee Slots + +| Name | Parameters | Description | Version | +| ------- | ---------- | ----------- | ------------ | +| default | `()` | Content. | NEXT_VERSION | diff --git a/src/marquee/demos/zhCN/auto-fill.demo.vue b/src/marquee/demos/zhCN/auto-fill.demo.vue new file mode 100644 index 00000000000..137fc842af3 --- /dev/null +++ b/src/marquee/demos/zhCN/auto-fill.demo.vue @@ -0,0 +1,16 @@ + +# 自动填充 + +使用 `auto-fill` 属性让内容铺满空白空间。 + + + diff --git a/src/marquee/demos/zhCN/basic.demo.vue b/src/marquee/demos/zhCN/basic.demo.vue new file mode 100644 index 00000000000..572e31c019c --- /dev/null +++ b/src/marquee/demos/zhCN/basic.demo.vue @@ -0,0 +1,11 @@ + +# 基础用法 + +在跑马灯中输入文字: + + + diff --git a/src/marquee/demos/zhCN/image.demo.vue b/src/marquee/demos/zhCN/image.demo.vue new file mode 100644 index 00000000000..534a9a8b0f8 --- /dev/null +++ b/src/marquee/demos/zhCN/image.demo.vue @@ -0,0 +1,16 @@ + +# 图片 + +你可以将任何内容放入跑马灯中。 + + + diff --git a/src/marquee/demos/zhCN/index.demo-entry.md b/src/marquee/demos/zhCN/index.demo-entry.md new file mode 100644 index 00000000000..e94c2872ebc --- /dev/null +++ b/src/marquee/demos/zhCN/index.demo-entry.md @@ -0,0 +1,28 @@ +# 跑马灯 Marquee + +我有一个高中同学,当时他的口头禅是:“滚滚滚。” + +自 `NEXT_VERSION` 开始提供。 + +## 演示 + +```demo +basic.vue +image.vue +auto-fill.vue +``` + +## API + +### Marquee Props + +| 名称 | 类型 | 默认值 | 说明 | 版本 | +| --- | --- | --- | --- | --- | +| auto-fill | `boolean` | `false` | 是否重复的用内容铺满容器的空白 | NEXT_VERSION | +| speed | `number` | `48` | 移动的速度,单位是像素每秒 | NEXT_VERSION | + +### Marquee Slots + +| 名称 | 参数 | 说明 | 版本 | +| ------- | ---- | ---- | ------------ | +| default | `()` | 内容 | NEXT_VERSION | diff --git a/src/marquee/index.ts b/src/marquee/index.ts new file mode 100644 index 00000000000..447b5537383 --- /dev/null +++ b/src/marquee/index.ts @@ -0,0 +1,2 @@ +export { default as NMarqueue } from './src/Marquee' +export type * from './src/public-types' diff --git a/src/marquee/src/Marquee.tsx b/src/marquee/src/Marquee.tsx new file mode 100644 index 00000000000..0ebd90fad49 --- /dev/null +++ b/src/marquee/src/Marquee.tsx @@ -0,0 +1,148 @@ +import { computed, defineComponent, h, nextTick, ref } from 'vue' +import { VResizeObserver } from 'vueuc' +import { repeat } from 'seemly' +import { useConfig, useTheme } from '../../_mixins' +import { marqueeLight } from '../styles' +import style from './styles/index.cssr' +import { marqueeProps } from './props' + +export default defineComponent({ + name: 'Marquee', + props: marqueeProps, + setup(props) { + const { mergedClsPrefixRef } = useConfig(props) + useTheme( + 'Marquee', + '-marquee', + style, + marqueeLight, + props, + mergedClsPrefixRef + ) + + const containerElRef = ref(null) + + const contentWidthRef = ref(-1) + const containerWidthRef = ref(-1) + + const playStateRef = ref<'paused' | 'running'>('running') + + const repeatCountInOneGroupRef = computed(() => { + if (!props.autoFill) + return 1 + const { value: contentWidth } = contentWidthRef + const { value: containerWidth } = containerWidthRef + if (contentWidth === -1 || containerWidth === -1) + return 1 + return Math.ceil(containerWidthRef.value / contentWidth) + }) + + const durationRef = computed(() => { + const { value: contentWidth } = contentWidthRef + if (contentWidth === -1) + return 0 + return (contentWidth * repeatCountInOneGroupRef.value) / props.speed + }) + + const animationCssVarsRef = computed(() => { + return { + '--n-play': playStateRef.value, + '--n-direction': 'normal', + '--n-duration': `${durationRef.value}s`, + '--n-delay': '0s', + '--n-iteration-count': 'infinite', + '--n-min-width': 'auto' + } + }) + + function resetScrollState() { + playStateRef.value = 'paused' + nextTick().then(() => { + void containerElRef.value?.offsetTop + playStateRef.value = 'running' + }) + } + + function handleContainerResize(entry: ResizeObserverEntry) { + containerWidthRef.value = entry.contentRect.width + } + + function handleContentResize(entry: ResizeObserverEntry) { + contentWidthRef.value = entry.contentRect.width + } + + function handleAnimationIteration() { + resetScrollState() + } + + return { + mergedClsPrefix: mergedClsPrefixRef, + animationCssVars: animationCssVarsRef, + containerElRef, + repeatCountInOneGroup: repeatCountInOneGroupRef, + handleContainerResize, + handleContentResize, + handleAnimationIteration + } + }, + render() { + const { + $slots, + mergedClsPrefix, + animationCssVars, + repeatCountInOneGroup, + handleAnimationIteration + } = this + const originalNode = ( + +
+ {$slots} +
+
+ ) + const mirrorNode = ( +
{$slots}
+ ) + if (this.autoFill) { + return ( + +
+
+ {originalNode} + {repeat(repeatCountInOneGroup - 1, mirrorNode)} +
+
+ {repeat(repeatCountInOneGroup, mirrorNode)} +
+
+
+ ) + } + else { + return ( +
+
+ {originalNode} +
+
{mirrorNode}
+
+ ) + } + } +}) diff --git a/src/marquee/src/props.ts b/src/marquee/src/props.ts new file mode 100644 index 00000000000..6317654d662 --- /dev/null +++ b/src/marquee/src/props.ts @@ -0,0 +1,15 @@ +import type { ThemeProps } from '../../_mixins' +import { useTheme } from '../../_mixins' +import type { ExtractPublicPropTypes } from '../../_utils' +import type { MarqueeTheme } from '../styles' + +export const marqueeProps = { + ...(useTheme.props as ThemeProps), + autoFill: Boolean, + speed: { + type: Number, + default: 48 + } +} + +export type MarqueeProps = ExtractPublicPropTypes diff --git a/src/marquee/src/public-types.ts b/src/marquee/src/public-types.ts new file mode 100644 index 00000000000..8ebd7af4cc3 --- /dev/null +++ b/src/marquee/src/public-types.ts @@ -0,0 +1 @@ +export type { MarqueeProps } from './props' diff --git a/src/marquee/src/styles/index.cssr.ts b/src/marquee/src/styles/index.cssr.ts new file mode 100644 index 00000000000..36828edd01c --- /dev/null +++ b/src/marquee/src/styles/index.cssr.ts @@ -0,0 +1,40 @@ +import { c, cB, cE, cNotM } from '../../../_utils/cssr' + +// vars: +// --n-play +// --n-direction +// --n-duration +// --n-delay +// --n-iteration-count +// --n-min-width +export default c([ + cB('marquee', ` + overflow: hidden; + display: flex; + `, [ + cE('group', ` + flex: 0 0 auto; + min-width: var(--n-min-width); + z-index: 1; + display: flex; + flex-direction: row; + align-items: center; + animation: n-marquee var(--n-duration) linear var(--n-delay) var(--n-iteration-count); + animation-play-state: var(--n-play); + animation-delay: var(--n-delay); + animation-direction: var(--n-direction); + `), + cNotM('auto-fill', [ + cE('group', `min-width: 100%;`), + cE('item', `min-width: 100%;`) + ]) + ]), + c('@keyframes n-marquee', { + from: { + transform: 'translateX(0)' + }, + to: { + transform: 'translateX(-100%)' + } + }) +]) diff --git a/src/marquee/styles/dark.ts b/src/marquee/styles/dark.ts new file mode 100644 index 00000000000..0c7f343e81b --- /dev/null +++ b/src/marquee/styles/dark.ts @@ -0,0 +1,11 @@ +import { commonDark } from '../../_styles/common' +import type { MarqueeTheme } from './light' +import { self } from './light' + +const marqueeDark: MarqueeTheme = { + name: 'Marquee', + common: commonDark, + self +} + +export default marqueeDark diff --git a/src/marquee/styles/index.ts b/src/marquee/styles/index.ts new file mode 100644 index 00000000000..84c49a7b958 --- /dev/null +++ b/src/marquee/styles/index.ts @@ -0,0 +1,3 @@ +export { default as marqueeDark } from './dark' +export { default as marqueeLight } from './light' +export type { MarqueeTheme, MarqueeThemeVars } from './light' diff --git a/src/marquee/styles/light.ts b/src/marquee/styles/light.ts new file mode 100644 index 00000000000..84ac6acc869 --- /dev/null +++ b/src/marquee/styles/light.ts @@ -0,0 +1,17 @@ +import { commonLight } from '../../_styles/common' +import type { Theme } from '../../_mixins' + +export function self() { + return {} +} + +export type MarqueeThemeVars = ReturnType + +const marqueeLight: Theme<'Marquee', MarqueeThemeVars> = { + name: 'Marquee', + common: commonLight, + self +} + +export default marqueeLight +export type MarqueeTheme = typeof marqueeLight diff --git a/src/marquee/tests/Marquee.spec.ts b/src/marquee/tests/Marquee.spec.ts new file mode 100644 index 00000000000..7109b7265dc --- /dev/null +++ b/src/marquee/tests/Marquee.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils' +import { NMarqueue } from '../index' + +describe('n-marquee', () => { + it('should work with import on demand', () => { + mount(NMarqueue) + }) +}) diff --git a/src/marquee/tests/server.spec.tsx b/src/marquee/tests/server.spec.tsx new file mode 100644 index 00000000000..3a4d7ae9773 --- /dev/null +++ b/src/marquee/tests/server.spec.tsx @@ -0,0 +1,20 @@ +/** + * @jest-environment node + */ +import { createSSRApp, h } from 'vue' +import { renderToString } from '@vue/server-renderer' +import { setup } from '@css-render/vue3-ssr' +import { NEmpty } from '../..' + +describe('sSR', () => { + it('works', async () => { + const app = createSSRApp(() => ) + setup(app) + try { + await renderToString(app) + } + catch (e) { + expect(e).not.toBeTruthy() + } + }) +}) diff --git a/src/themes/dark.ts b/src/themes/dark.ts index 49cf3af008c..9fdf8e2930e 100644 --- a/src/themes/dark.ts +++ b/src/themes/dark.ts @@ -85,6 +85,7 @@ import { watermarkDark } from '../watermark/styles' import { splitDark } from '../split/styles' import { flexDark } from '../styles' import { floatButtonGroupDark } from '../float-button-group/styles' +import { marqueeDark } from '../marquee/styles' import type { BuiltInGlobalTheme } from './interface' export const darkTheme: BuiltInGlobalTheme = { @@ -175,5 +176,6 @@ export const darkTheme: BuiltInGlobalTheme = { Watermark: watermarkDark, Split: splitDark, FloatButton: floatButtonDark, - FloatButtonGroup: floatButtonGroupDark + FloatButtonGroup: floatButtonGroupDark, + Marquee: marqueeDark } diff --git a/src/themes/light.ts b/src/themes/light.ts index a3cc8e929ee..b63fcef7735 100644 --- a/src/themes/light.ts +++ b/src/themes/light.ts @@ -87,6 +87,7 @@ import { watermarkLight } from '../watermark/styles' import { splitLight } from '../split/styles' import { flexLight } from '../flex/styles' import { floatButtonGroupLight } from '../float-button-group/styles' +import { marqueeLight } from '../marquee/styles' import type { BuiltInGlobalTheme } from './interface' export const lightTheme: BuiltInGlobalTheme = { @@ -177,5 +178,6 @@ export const lightTheme: BuiltInGlobalTheme = { Watermark: watermarkLight, Split: splitLight, FloatButton: floatButtonLight, - FloatButtonGroup: floatButtonGroupLight + FloatButtonGroup: floatButtonGroupLight, + Marquee: marqueeLight }