diff --git a/src/packages/noticebar/__test__/noticebar.spec.tsx b/src/packages/noticebar/__test__/noticebar.spec.tsx index 628de43f83..367e19a15e 100644 --- a/src/packages/noticebar/__test__/noticebar.spec.tsx +++ b/src/packages/noticebar/__test__/noticebar.spec.tsx @@ -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' @@ -260,3 +261,94 @@ test('vertical container height calculation with children', async () => { { 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 ( + + {list.map((item, index) => ( +
+ {item} +
+ ))} +
+ ) + } + + const { container } = render() + + // 等待初始渲染完成 + 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`) + }) + }) +}) diff --git a/src/packages/noticebar/demo.taro.tsx b/src/packages/noticebar/demo.taro.tsx index 0e90655208..d88a05ef24 100644 --- a/src/packages/noticebar/demo.taro.tsx +++ b/src/packages/noticebar/demo.taro.tsx @@ -29,7 +29,7 @@ const NoticeBarDemo = () => { customeRight: '自定义右侧内容', vertical: '垂直滚动', complexAm: '纵向模式:自定义左侧图标', - customAm: '纵向模式:自定义滚动内容', + customAm: '纵向模式:自定义滚动内容,动态变更滚动内容', customRightIcon: '纵向模式:自定义右侧图标', }, 'en-US': { @@ -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', }, }) diff --git a/src/packages/noticebar/demo.tsx b/src/packages/noticebar/demo.tsx index 62b293d360..29a1931541 100644 --- a/src/packages/noticebar/demo.tsx +++ b/src/packages/noticebar/demo.tsx @@ -25,7 +25,7 @@ const NoticeBarDemo = () => { customeRight: '自定义右侧内容', vertical: '垂直滚动', complexAm: '纵向模式:自定义左侧图标', - customAm: '纵向模式:自定义滚动内容', + customAm: '纵向模式:自定义滚动内容,动态变更滚动内容', customRightIcon: '纵向模式:自定义右侧图标', }, 'en-US': { @@ -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', }, }) diff --git a/src/packages/noticebar/demos/h5/demo10.tsx b/src/packages/noticebar/demos/h5/demo10.tsx index 21907f9911..646c5299c3 100644 --- a/src/packages/noticebar/demos/h5/demo10.tsx +++ b/src/packages/noticebar/demos/h5/demo10.tsx @@ -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 = [ @@ -9,6 +9,8 @@ const Demo9 = () => { 'CheckBox 复选按钮', ] + const [list, setList] = useState(horseLamp3) + return ( <> { console.log('close') }} > - {horseLamp3.map((item, index) => { + {list.map((item, index) => { return (
{ ) })} + + + + + + ) } diff --git a/src/packages/noticebar/demos/taro/demo10.tsx b/src/packages/noticebar/demos/taro/demo10.tsx index 2ed60f3ae4..6958c3e405 100644 --- a/src/packages/noticebar/demos/taro/demo10.tsx +++ b/src/packages/noticebar/demos/taro/demo10.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import { NoticeBar } from '@nutui/nutui-react-taro' +import React, { useState } from 'react' +import { Button, NoticeBar, Space } from '@nutui/nutui-react-taro' const Demo10 = () => { const horseLamp3 = [ @@ -9,6 +9,8 @@ const Demo10 = () => { 'CheckBox 复选按钮', ] + const [list, setList] = useState(horseLamp3) + return ( <> { console.log('close') }} > - {horseLamp3.map((item, index) => { + {list.map((item, index) => { return (
{ ) })} + + + + + + ) } diff --git a/src/packages/noticebar/doc.en-US.md b/src/packages/noticebar/doc.en-US.md index 5ba674fe7a..41a99383aa 100644 --- a/src/packages/noticebar/doc.en-US.md +++ b/src/packages/noticebar/doc.en-US.md @@ -88,7 +88,7 @@ Add Right mode to set more custom content. ::: -### Vertical Scroll Custom Style +### Vertical Scroll Custom Style, Dynamic Content Updates :::demo diff --git a/src/packages/noticebar/doc.md b/src/packages/noticebar/doc.md index ac3b4ca0a1..1b5a4c219e 100644 --- a/src/packages/noticebar/doc.md +++ b/src/packages/noticebar/doc.md @@ -96,7 +96,7 @@ import { NoticeBar } from '@nutui/nutui-react' ::: -### 纵向模式:自定义右侧图标 +### 纵向模式:自定义右侧图标,动态变更滚动内容 :::demo diff --git a/src/packages/noticebar/doc.taro.md b/src/packages/noticebar/doc.taro.md index 614ff121da..8082268a37 100644 --- a/src/packages/noticebar/doc.taro.md +++ b/src/packages/noticebar/doc.taro.md @@ -96,7 +96,7 @@ import { NoticeBar } from '@nutui/nutui-react-taro' ::: -### 纵向模式:自定义右侧图标 +### 纵向模式:自定义右侧图标,动态变更滚动内容 :::demo diff --git a/src/packages/noticebar/doc.zh-TW.md b/src/packages/noticebar/doc.zh-TW.md index 82ea4ccb18..4658324427 100644 --- a/src/packages/noticebar/doc.zh-TW.md +++ b/src/packages/noticebar/doc.zh-TW.md @@ -96,7 +96,7 @@ import { NoticeBar } from '@nutui/nutui-react' ::: -### 縱嚮模式:自定義右側圖標 +### 縱嚮模式:自定義右側圖標,動態變更滾動內容 :::demo diff --git a/src/packages/noticebar/noticebar.taro.tsx b/src/packages/noticebar/noticebar.taro.tsx index 03a349cf38..1a36175485 100644 --- a/src/packages/noticebar/noticebar.taro.tsx +++ b/src/packages/noticebar/noticebar.taro.tsx @@ -4,6 +4,7 @@ import React, { useMemo, useRef, useState, + useCallback, } from 'react' import { ITouchEvent, View } from '@tarojs/components' import { Close, Notice } from '@nutui/icons-react-taro' @@ -78,9 +79,10 @@ export const NoticeBar: FunctionComponent< const [isCanScroll, SetIsCanScroll] = useState(null) const isVertical = direction === 'vertical' const [rect, setRect] = useState(null as any | null) - let active = 0 + const activeRef = useRef(0) const [ready, setReady] = useState(false) const container = useRef(null) + const [isContainerReady, setIsContainerReady] = useState(false) const innerRef = useRef(null) const swiperRef = useRef({ moving: false, @@ -131,7 +133,7 @@ export const NoticeBar: FunctionComponent< // 销毁事件 clearInterval(timer) } - }, []) + }, [childs]) useEffect(() => { initScrollWrap(content) @@ -249,28 +251,31 @@ export const NoticeBar: FunctionComponent< } // 垂直自定义滚动方式 - const init = (active = +0) => { + const init = (activeIndex = 0) => { if (!container?.current) return setTimeout(async () => { const rects = await getRectInMultiPlatform(container?.current) - const _active = Math.max(Math.min(childCount - 1, active), 0) + const _active = Math.max(Math.min(childCount - 1, activeIndex), 0) const _height = rects?.height trackSize = childCount * Number(_height) const targetOffset = getOffset(_active) - swiperRef.current.moving = true - if (ready) { - swiperRef.current.moving = false - } - active = _active + + activeRef.current = _active setRect(rects) setOffset(targetOffset) + setChildOffset(new Array(childCount).fill(0)) + + // 关闭动画并立即设置样式 + swiperRef.current.moving = true + setStyle(targetOffset, rects) + + swiperRef.current.moving = false setReady(true) }, 0) } useEffect(() => { if (ready) { - stopAutoPlay() autoplay() } return () => { @@ -279,16 +284,17 @@ export const NoticeBar: FunctionComponent< }, [ready]) useEffect(() => { - if (isVertical && children) { - init() + if (isVertical && children && isContainerReady) { stopAutoPlay() - autoplay() + setReady(false) + init() } - }, [children, container?.current]) + }, [children, isContainerReady]) // 清除定时器 const stopAutoPlay = () => { clearTimeout(swiperRef.current.autoplayTimer) + swiperRef.current.moving = false swiperRef.current.autoplayTimer = null } // 定时轮播 @@ -310,28 +316,39 @@ export const NoticeBar: FunctionComponent< const targetActive = getActive(pace) // 父级容器偏移量 const targetOffset = getOffset(targetActive, offset) - // 如果循环,调整开头结尾图片位置 - if (Array.isArray(children) && children[0] && targetOffset !== minOffset) { - const rightBound = targetOffset < minOffset - childOffset[0] = rightBound ? trackSize : 0 - } - if ( - Array.isArray(children) && - children[childCount - 1] && - targetOffset !== 0 - ) { - const leftBound = targetOffset > 0 - childOffset[childCount - 1] = leftBound ? -trackSize : 0 - } - setChildOffset(childOffset) - active = targetActive + + // 循环滚动,调整开头结尾图片位置 + setChildOffset((prevChildOffset) => { + const newChildOffset = [...prevChildOffset] + + if ( + Array.isArray(children) && + children[0] && + targetOffset !== minOffset + ) { + const rightBound = targetOffset < minOffset + newChildOffset[0] = rightBound ? trackSize : 0 + } + if ( + Array.isArray(children) && + children[childCount - 1] && + targetOffset !== 0 + ) { + const leftBound = targetOffset > 0 + newChildOffset[childCount - 1] = leftBound ? -trackSize : 0 + } + + return newChildOffset + }) + + activeRef.current = targetActive setOffset(targetOffset) - getStyle(targetOffset) + setStyle(targetOffset) } // 下一页 const next = () => { - resettPosition() + resetPosition() requestFrame(() => { requestFrame(() => { swiperRef.current.moving = false @@ -345,15 +362,17 @@ export const NoticeBar: FunctionComponent< onItemClick && onItemClick(event, value) } - const getStyle = (moveOffset = offset) => { + const setStyle = (moveOffset = offset, currentRect = rect) => { const target = innerRef.current if (!target) { return } let _offset = 0 - // 容器高度-元素高度 - const val = (rect?.height || 0) - height - _offset = moveOffset + Number(active === childCount - 1 && val / 2) + const containerHeight = currentRect?.height || height + const val = containerHeight - Number(height) + + _offset = + moveOffset + Number(activeRef.current === childCount - 1 && val / 2) target.style.transitionDuration = `${ swiperRef.current.moving ? 0 : duration @@ -361,6 +380,7 @@ export const NoticeBar: FunctionComponent< target.style.height = `${Number(height) * (childCount + 1)}px` target.style.transform = `translate3D(0,${_offset}px,0)` } + // 无缝滚动第一个元素位移控制 const itemStyle = (index: any) => { const style: any = {} @@ -377,11 +397,12 @@ export const NoticeBar: FunctionComponent< // 确定当前active 元素 const getActive = (pace: number) => { + const currentActive = activeRef.current if (pace) { - const _active = active + pace + const _active = currentActive + pace return range(_active, -1, childCount) } - return active + return currentActive } // 计算位移 const getOffset = (active: number, offset = 0) => { @@ -398,12 +419,13 @@ export const NoticeBar: FunctionComponent< return Math.min(Math.max(num, min), max) } // 重置首尾位置信息 - const resettPosition = () => { + const resetPosition = () => { + const currentActive = activeRef.current swiperRef.current.moving = true - if (active <= -1) { + if (currentActive <= -1) { move({ pace: childCount }) } - if (active >= childCount) { + if (currentActive >= childCount) { move({ pace: -childCount }) } } @@ -421,6 +443,18 @@ export const NoticeBar: FunctionComponent< stopAutoPlay() } }, []) + + const hasVerticalContent = useMemo(() => { + if (!isVertical) return false + if (children) return childs?.length > 0 + return list?.length > 0 + }, [isVertical, childs, list, children]) + + const containerRefCallback = useCallback((ref: any) => { + container.current = ref + setIsContainerReady(true) + }, []) + return ( {showNoticeBar && direction === 'horizontal' ? ( @@ -454,11 +488,11 @@ export const NoticeBar: FunctionComponent< ) : null} ) : null} - {showNoticeBar && scrollList.current.length > 0 && isVertical ? ( + {showNoticeBar && hasVerticalContent && isVertical ? ( {leftIcon ? ( diff --git a/src/packages/noticebar/noticebar.tsx b/src/packages/noticebar/noticebar.tsx index 7d86946d40..a371e56538 100644 --- a/src/packages/noticebar/noticebar.tsx +++ b/src/packages/noticebar/noticebar.tsx @@ -5,6 +5,7 @@ import React, { useMemo, useRef, useState, + useCallback, } from 'react' import { Close, Notice } from '@nutui/icons-react' import classNames from 'classnames' @@ -78,9 +79,10 @@ export const NoticeBar: FunctionComponent< const [isCanScroll, SetIsCanScroll] = useState(null) const isVertical = direction === 'vertical' const [rect, setRect] = useState(null as any | null) - let active = 0 + const activeRef = useRef(0) const [ready, setReady] = useState(false) const container = useRef(null) + const [isContainerReady, setIsContainerReady] = useState(false) const innerRef = useRef(null) const swiperRef = useRef({ moving: false, @@ -131,7 +133,7 @@ export const NoticeBar: FunctionComponent< // 销毁事件 clearInterval(timer) } - }, []) + }, [childs]) useEffect(() => { initScrollWrap(content) @@ -247,25 +249,28 @@ export const NoticeBar: FunctionComponent< } // 垂直自定义滚动方式 - const init = (active = +0) => { + const init = (activeIndex = 0) => { const rects = getRect(container?.current) - const _active = Math.max(Math.min(childCount - 1, active), 0) + const _active = Math.max(Math.min(childCount - 1, activeIndex), 0) const _height = rects.height trackSize = childCount * Number(_height) const targetOffset = getOffset(_active) - swiperRef.current.moving = true - if (ready) { - swiperRef.current.moving = false - } - active = _active + + activeRef.current = _active setRect(rects) setOffset(targetOffset) + setChildOffset(new Array(childCount).fill(0)) + + // 关闭动画并立即设置样式 + swiperRef.current.moving = true + setStyle(targetOffset, rects) + + swiperRef.current.moving = false setReady(true) } useEffect(() => { if (ready) { - stopAutoPlay() autoplay() } return () => { @@ -274,16 +279,17 @@ export const NoticeBar: FunctionComponent< }, [ready]) useEffect(() => { - if (isVertical && children) { - init() + if (isVertical && children && isContainerReady) { stopAutoPlay() - autoplay() + setReady(false) + init() } - }, [children, container?.current]) + }, [children, isContainerReady]) // 清除定时器 const stopAutoPlay = () => { clearTimeout(swiperRef.current.autoplayTimer) + swiperRef.current.moving = false swiperRef.current.autoplayTimer = null } // 定时轮播 @@ -305,28 +311,39 @@ export const NoticeBar: FunctionComponent< const targetActive = getActive(pace) // 父级容器偏移量 const targetOffset = getOffset(targetActive, offset) + // 循环滚动,调整开头结尾图片位置 - if (Array.isArray(children) && children[0] && targetOffset !== minOffset) { - const rightBound = targetOffset < minOffset - childOffset[0] = rightBound ? trackSize : 0 - } - if ( - Array.isArray(children) && - children[childCount - 1] && - targetOffset !== 0 - ) { - const leftBound = targetOffset > 0 - childOffset[childCount - 1] = leftBound ? -trackSize : 0 - } - setChildOffset(childOffset) - active = targetActive + setChildOffset((prevChildOffset) => { + const newChildOffset = [...prevChildOffset] + + if ( + Array.isArray(children) && + children[0] && + targetOffset !== minOffset + ) { + const rightBound = targetOffset < minOffset + newChildOffset[0] = rightBound ? trackSize : 0 + } + if ( + Array.isArray(children) && + children[childCount - 1] && + targetOffset !== 0 + ) { + const leftBound = targetOffset > 0 + newChildOffset[childCount - 1] = leftBound ? -trackSize : 0 + } + + return newChildOffset + }) + + activeRef.current = targetActive setOffset(targetOffset) - getStyle(targetOffset) + setStyle(targetOffset) } // 下一页 const next = () => { - resettPosition() + resetPosition() requestFrame(() => { requestFrame(() => { swiperRef.current.moving = false @@ -340,15 +357,18 @@ export const NoticeBar: FunctionComponent< onItemClick && onItemClick(event, value) } - const getStyle = (moveOffset = offset) => { + const setStyle = (moveOffset = offset, currentRect = rect) => { const target = innerRef.current if (!target) { return } let _offset = 0 - // 容器高度-元素高度 - const val = rect.height - height - _offset = moveOffset + Number(active === childCount - 1 && val / 2) + // 使用传入的 currentRect 或状态中的 rect + const containerHeight = currentRect?.height || height + const val = containerHeight - Number(height) + + _offset = + moveOffset + Number(activeRef.current === childCount - 1 && val / 2) target.style.transitionDuration = `${ swiperRef.current.moving ? 0 : duration @@ -372,11 +392,12 @@ export const NoticeBar: FunctionComponent< // 确定当前active 元素 const getActive = (pace: number) => { + const currentActive = activeRef.current if (pace) { - const _active = active + pace + const _active = currentActive + pace return range(_active, -1, childCount) } - return active + return currentActive } // 计算位移 const getOffset = (active: number, offset = 0) => { @@ -393,12 +414,13 @@ export const NoticeBar: FunctionComponent< return Math.min(Math.max(num, min), max) } // 重置首尾位置信息 - const resettPosition = () => { + const resetPosition = () => { + const currentActive = activeRef.current swiperRef.current.moving = true - if (active <= -1) { + if (currentActive <= -1) { move({ pace: childCount }) } - if (active >= childCount) { + if (currentActive >= childCount) { move({ pace: -childCount }) } } @@ -416,6 +438,18 @@ export const NoticeBar: FunctionComponent< stopAutoPlay() } }, []) + + const hasVerticalContent = useMemo(() => { + if (!isVertical) return false + if (children) return childs?.length > 0 + return list?.length > 0 + }, [isVertical, childs, list, children]) + + const containerRefCallback = useCallback((ref: any) => { + container.current = ref + setIsContainerReady(true) + }, []) + return (
{showNoticeBar && direction === 'horizontal' ? ( @@ -446,11 +480,11 @@ export const NoticeBar: FunctionComponent< ) : null}
) : null} - {showNoticeBar && scrollList.current.length > 0 && isVertical ? ( + {showNoticeBar && hasVerticalContent && isVertical ? (
{leftIcon ? (