Skip to content

fix(noticebar): the speed and duration attributes behave incorrectly in vertical mode #3252

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

Open
wants to merge 3 commits into
base: feat_v3.x
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
253 changes: 252 additions & 1 deletion src/packages/noticebar/__test__/noticebar.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import { useState } from 'react'
import { render, fireEvent, waitFor, act } from '@testing-library/react'
import '@testing-library/jest-dom'
import { Fabulous } from '@nutui/icons-react'
Expand Down Expand Up @@ -168,7 +169,7 @@ test('vertical test move', async () => {
await act(() => {
expect(
container.querySelector('.nut-noticebar-box-horseLamp-list')
).toHaveAttribute('style', 'transition: all 0.8s; margin-top: -30px;')
).not.toHaveAttribute('style')
})
})

Expand Down Expand Up @@ -220,3 +221,253 @@ test('vertical event test', async () => {
fireEvent.click(box)
await waitFor(() => expect(handleClick).toBeCalled())
})

test('vertical container height calculation with children', async () => {
const horseLamp1 = [
'NoticeBar 公告栏',
'Cascader 级联选择',
'DatePicker 日期选择器',
'CheckBox 复选按钮',
]
const height = 50
const { container } = render(
<NoticeBar direction="vertical" height={height} speed={10} duration={1000}>
{horseLamp1.map((item, index) => {
return (
<div
className="custom-item"
style={{ height: `${height}px`, lineHeight: `${height}px` }}
key={index}
>
{item}
</div>
)
})}
</NoticeBar>
)

await waitFor(
() => {
const wrapElement = container.querySelector('.nut-noticebar-box-wrap')
if (wrapElement) {
// 验证容器高度应该是 (childCount + 1) * height
// childCount = 4, height = 50, 所以期望高度是 (4 + 1) * 50 = 250px
const expectedHeight = `${(horseLamp1.length + 1) * height}px`
expect(wrapElement).toHaveStyle(`height: ${expectedHeight}`)
}
},
// 由于init中并不会立刻设置样式,所以需要等待一段时间
{ timeout: 3000 }
)
})

test('dynamic children update test', async () => {
let setList: any
const height = 40

const TestComponent = () => {
const [list, updateList] = useState(['原始项目1', '原始项目2', '原始项目3'])
setList = updateList

return (
<NoticeBar direction="vertical" height={height} speed={10} duration={500}>
{list.map((item, index) => (
<div
className="custom-item"
style={{ height: `${height}px`, lineHeight: `${height}px` }}
key={index}
>
{item}
</div>
))}
</NoticeBar>
)
}

const { container } = render(<TestComponent />)

// 等待初始渲染完成
await waitFor(() => {
const wrapElement = container.querySelector('.nut-noticebar-box-wrap')
expect(wrapElement).toHaveStyle('height: 160px') // (3 + 1) * 40

// 1. 初始时容器的垂直位移为0(显示第一项)
expect(wrapElement).toHaveStyle('transform: translate3D(0,0px,0)')

const items = container.querySelectorAll('.custom-item')
expect(items).toHaveLength(3)
expect(items[0]).toHaveTextContent('原始项目1')
})

// 等待轮播进行一段时间,确保当前不是第一项
await waitFor(
() => {
const wrapElement = container.querySelector('.nut-noticebar-box-wrap')
const transform = wrapElement
?.getAttribute('style')
?.match(/transform:\s*translate3D\(([^,]+),([^,]+),([^)]+)\)/)
const yOffset = transform ? transform[2].trim() : '0px'

// 验证已经轮播到非第一项(垂直偏移不为0)
expect(yOffset).not.toBe('0px')
},
{ timeout: 2000 }
) // 给足够时间让轮播发生

// 变更列表数据
act(() => {
setList(['新项目A', '新项目B', '新项目C', '新项目D'])
})

await waitFor(() => {
// 验证容器高度更新为新的计算值
const wrapElement = container.querySelector('.nut-noticebar-box-wrap')
expect(wrapElement).toHaveStyle('height: 200px') // (4 + 1) * 40

// 验证变更后重置回第一项:
// 1. 容器的垂直位移重置为0
expect(wrapElement).toHaveStyle('transform: translate3D(0,0px,0)')

// 2. 第一个子项没有额外的transform
const firstItem = container.querySelector(
'.custom-item:first-child'
) as HTMLElement
expect(firstItem.style.transform).toBe('')

// 验证元素结构更新:应该有4个项目
const items = container.querySelectorAll('.custom-item')
expect(items).toHaveLength(4)

// 验证当前显示的是新列表的第一项内容
expect(items[0]).toHaveTextContent('新项目A')
expect(items[1]).toHaveTextContent('新项目B')
expect(items[2]).toHaveTextContent('新项目C')
expect(items[3]).toHaveTextContent('新项目D')

// 验证样式更新:每个item的高度样式
items.forEach((item) => {
expect(item).toHaveStyle(`height: ${height}px`)
expect(item).toHaveStyle(`line-height: ${height}px`)
})
})
})

describe('NoticeBar Vertical Scrolling with refined timing logic', () => {
beforeEach(() => {
vi.useFakeTimers()
})

afterEach(() => {
vi.useRealTimers()
})

// 测试 list Prop 模式下的时间和动画
test('list mode: should correctly display items based on scrollList and style', async () => {
const listData = ['公告1', '公告2', '公告3']
const itemHeight = 30
const animationSpeed = 15
const calculatedAnimationTime = (30 / 15 / 4) * 1000
const pauseDuration = 2000

const { container } = render(
<NoticeBar
list={listData}
direction="vertical"
height={itemHeight}
speed={animationSpeed}
duration={pauseDuration}
/>
)

const listElement = container.querySelector<HTMLElement>(
'.nut-noticebar-box-horseLamp-list'
)
expect(listElement).toBeInTheDocument()
let items = listElement?.querySelectorAll(
'.nut-noticebar-box-horseLamp-list-item'
)

// 初始状态,显示第一项 "公告1"
expect(items?.[0]).toHaveTextContent('公告1')

// 第一次滚动
act(() => {
vi.advanceTimersByTime(pauseDuration + 1)
})
await waitFor(() => {
expect(listElement).toHaveStyle(`margin-top: -${itemHeight}px`)
expect(listElement).toHaveStyle(
`transition: all ${calculatedAnimationTime}ms`
)
})

// 动画结束并且开始下一轮
act(() => {
vi.advanceTimersByTime(calculatedAnimationTime - 1)
})
await waitFor(() => {
expect(listElement).not.toHaveStyle(`margin-top: -${itemHeight}px`)
items = listElement?.querySelectorAll(
'.nut-noticebar-box-horseLamp-list-item'
)
// 内部 scrollList.current 变为 ['公告2', '公告3', '公告1']
expect(items?.[0]).toHaveTextContent('公告2')
expect(items?.[1]).toHaveTextContent('公告3')
expect(items?.[2]).toHaveTextContent('公告1')
})
})

// 测试 children Prop 模式下的时间和动画
test('children mode: should use duration as pause, speed/height for animation', async () => {
const itemHeight = 30
const animationSpeed = 15
const calculatedAnimationTime = (30 / 15 / 4) * 1000
const pauseDuration = 2000

const { container } = render(
<NoticeBar
direction="vertical"
height={itemHeight}
speed={animationSpeed}
duration={pauseDuration}
>
<div className="child-item">公告1</div>
<div className="child-item">公告2</div>
<div className="child-item">公告3</div>
</NoticeBar>
)

const listElement = container.querySelector<HTMLElement>(
'.nut-noticebar-box-wrap'
)
expect(listElement).toBeInTheDocument()

// 第一次滚动
act(() => {
vi.advanceTimersByTime(pauseDuration + 1)
})

await waitFor(() => {
expect(listElement).toHaveStyle(
`transition-duration: ${calculatedAnimationTime}ms`
)
expect(listElement).toHaveStyle(
`transform: translate3D(0,-${itemHeight}px,0)`
)
})

// 开始下一轮滚动
act(() => {
vi.advanceTimersByTime(calculatedAnimationTime + pauseDuration)
})

await waitFor(() => {
expect(listElement).toHaveStyle(
`transition-duration: ${calculatedAnimationTime}ms`
)
expect(listElement).toHaveStyle(
`transform: translate3D(0,-${2 * itemHeight}px,0)`
)
})
})
})
4 changes: 2 additions & 2 deletions src/packages/noticebar/demo.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const NoticeBarDemo = () => {
customeRight: '自定义右侧内容',
vertical: '垂直滚动',
complexAm: '纵向模式:自定义左侧图标',
customAm: '纵向模式:自定义滚动内容',
customAm: '纵向模式:自定义滚动内容,动态变更滚动内容',
customRightIcon: '纵向模式:自定义右侧图标',
},
'en-US': {
Expand All @@ -42,7 +42,7 @@ const NoticeBarDemo = () => {
customeRight: 'custom right content',
vertical: 'Vertical Scroll',
complexAm: 'Vertical Scroll Complex Animation',
customAm: 'Vertical Scroll Custom Style',
customAm: 'Vertical Scroll Custom Style,Dynamic Change Scroll Content',
customRightIcon: 'Vertical Scroll Custom Right Icon',
},
})
Expand Down
4 changes: 2 additions & 2 deletions src/packages/noticebar/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const NoticeBarDemo = () => {
customeRight: '自定义右侧内容',
vertical: '垂直滚动',
complexAm: '纵向模式:自定义左侧图标',
customAm: '纵向模式:自定义滚动内容',
customAm: '纵向模式:自定义滚动内容,动态变更滚动内容',
customRightIcon: '纵向模式:自定义右侧图标',
},
'en-US': {
Expand All @@ -38,7 +38,7 @@ const NoticeBarDemo = () => {
customeRight: 'custom right content',
vertical: 'Vertical Scroll',
complexAm: 'Vertical Scroll Complex Animation',
customAm: 'Vertical Scroll Custom Style',
customAm: 'Vertical Scroll Custom Style,Dynamic Change Scroll Content',
customRightIcon: 'Vertical Scroll Custom Right Icon',
},
})
Expand Down
30 changes: 26 additions & 4 deletions src/packages/noticebar/demos/h5/demo10.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { NoticeBar } from '@nutui/nutui-react'
import React, { useState } from 'react'
import { Button, NoticeBar, Space } from '@nutui/nutui-react'

const Demo9 = () => {
const horseLamp3 = [
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

组件命名不一致。

组件名为 Demo9 但文件名为 demo10.tsx,这可能会造成混淆。

-const Demo9 = () => {
+const Demo10 = () => {
-export default Demo9
+export default Demo10
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Demo9 = () => {
const Demo10 = () => {
// existing implementation
};
export default Demo10;
🤖 Prompt for AI Agents
In src/packages/noticebar/demos/h5/demo10.tsx at line 4, the component is named
Demo9, which is inconsistent with the file name demo10.tsx. Rename the component
from Demo9 to Demo10 to match the file name and avoid confusion.

Expand All @@ -9,19 +9,21 @@ const Demo9 = () => {
'CheckBox 复选按钮',
]

const [list, setList] = useState(horseLamp3)

return (
<>
<NoticeBar
direction="vertical"
height={50}
speed={10}
duration={1000}
duration={3000}
closeable
onClose={() => {
console.log('close')
}}
>
{horseLamp3.map((item, index) => {
{list.map((item, index) => {
return (
<div
className="custom-item"
Expand All @@ -36,6 +38,26 @@ const Demo9 = () => {
)
})}
</NoticeBar>

<Space style={{ marginTop: '10px' }}>
<Button
size="small"
onClick={() => {
setList((prev) => [...prev, `${prev.length + 1}`])
}}
>
添加最后一项
</Button>

<Button
size="small"
onClick={() => {
setList((prev) => prev.slice(0, -1))
}}
>
删除最后一项
</Button>
</Space>
</>
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/packages/noticebar/demos/h5/demo11.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Demo10 = () => {
direction="vertical"
list={horseLamp1}
speed={10}
duration={1000}
duration={3000}
onItemClick={(e, v) => {
console.log('onclick-custom', v)
}}
Expand Down
Loading
Loading