|
1 | 1 | import { clsx } from "clsx"; |
2 | 2 | import PropTypes from "prop-types"; |
3 | | -import { Children, PureComponent, cloneElement } from "react"; |
| 3 | +import { |
| 4 | + Children, |
| 5 | + cloneElement, |
| 6 | + memo, |
| 7 | + useCallback, |
| 8 | + useEffect, |
| 9 | + useRef, |
| 10 | + useState, |
| 11 | +} from "react"; |
4 | 12 |
|
5 | | -export default class TextRotater extends PureComponent { |
6 | | - static defaultProps = { |
7 | | - delay: 0, |
8 | | - repeatDelay: 3000, |
9 | | - }; |
| 13 | +function TextRotater({ children, delay = 0, repeatDelay = 3000, maxWidth }) { |
| 14 | + const [currentIndex, setCurrentIndex] = useState(0); |
| 15 | + const [contentHeight, setContentHeight] = useState(0); |
| 16 | + const [isAnimating, setIsAnimating] = useState(false); |
| 17 | + const contentNodeRef = useRef(null); |
| 18 | + const heightTimeoutRef = useRef(null); |
| 19 | + const animationTimeoutRef = useRef(null); |
| 20 | + const repeatTimeoutRef = useRef(null); |
10 | 21 |
|
11 | | - static propTypes = { |
12 | | - children: PropTypes.arrayOf(PropTypes.node), |
13 | | - delay: PropTypes.number, |
14 | | - repeatDelay: PropTypes.number, |
15 | | - // Needed to prevent jump when |
16 | | - // rotating between texts of different widths |
17 | | - maxWidth: PropTypes.number, |
18 | | - }; |
| 22 | + const contentCallbackRef = useCallback((node) => { |
| 23 | + contentNodeRef.current = node; |
| 24 | + }, []); |
19 | 25 |
|
20 | | - state = { |
21 | | - currentIndex: 0, |
22 | | - contentHeight: 0, |
23 | | - isAnimating: false, |
24 | | - }; |
| 26 | + useEffect(() => { |
| 27 | + const calculateHeight = () => { |
| 28 | + if (contentNodeRef.current) { |
| 29 | + setContentHeight(contentNodeRef.current.clientHeight); |
| 30 | + } |
| 31 | + }; |
25 | 32 |
|
26 | | - render() { |
27 | | - const { children, maxWidth } = this.props; |
28 | | - const { currentIndex, contentHeight, isAnimating } = this.state; |
| 33 | + heightTimeoutRef.current = setTimeout(calculateHeight, 50); |
| 34 | + animationTimeoutRef.current = setTimeout(() => setIsAnimating(true), delay); |
| 35 | + |
| 36 | + window.addEventListener("resize", calculateHeight); |
| 37 | + |
| 38 | + return () => { |
| 39 | + clearTimeout(heightTimeoutRef.current); |
| 40 | + clearTimeout(animationTimeoutRef.current); |
| 41 | + clearTimeout(repeatTimeoutRef.current); |
| 42 | + window.removeEventListener("resize", calculateHeight); |
| 43 | + }; |
| 44 | + }, []); // eslint-disable-line react-hooks/exhaustive-deps |
| 45 | + |
| 46 | + const handleTransitionEnd = useCallback(() => { |
29 | 47 | const childrenCount = Children.count(children); |
| 48 | + setCurrentIndex((prev) => (prev + 1) % childrenCount); |
| 49 | + setIsAnimating(false); |
30 | 50 |
|
31 | | - const currentChild = cloneElement(children[currentIndex], { |
32 | | - ref: (child) => (this.content = child), |
33 | | - }); |
| 51 | + repeatTimeoutRef.current = setTimeout(() => { |
| 52 | + setIsAnimating(true); |
| 53 | + }, repeatDelay); |
| 54 | + }, [children, repeatDelay]); |
34 | 55 |
|
35 | | - const nextChild = cloneElement( |
36 | | - children[(currentIndex + 1) % childrenCount], |
37 | | - ); |
| 56 | + const childrenCount = Children.count(children); |
| 57 | + const nextChild = cloneElement(children[(currentIndex + 1) % childrenCount]); |
38 | 58 |
|
39 | | - return ( |
| 59 | + return ( |
| 60 | + <div |
| 61 | + className=" |
| 62 | + relative inline-block overflow-hidden align-bottom px-[0.3em] |
| 63 | + " |
| 64 | + > |
40 | 65 | <div |
41 | | - className=" |
42 | | - relative inline-block overflow-hidden align-bottom px-[0.3em] |
43 | | - " |
| 66 | + className={clsx( |
| 67 | + "inline-flex flex-col text-left", |
| 68 | + isAnimating && "text-rotater--slide-up", |
| 69 | + )} |
| 70 | + onTransitionEnd={handleTransitionEnd} |
| 71 | + style={{ height: contentHeight, width: maxWidth }} |
44 | 72 | > |
45 | | - <div |
46 | | - className={clsx( |
47 | | - "inline-flex flex-col text-left", |
48 | | - isAnimating && "text-rotater--slide-up", |
49 | | - )} |
50 | | - onTransitionEnd={this._handleTransitionEnd} |
51 | | - style={{ height: contentHeight, width: maxWidth }} |
52 | | - > |
53 | | - {currentChild} |
54 | | - {nextChild} |
55 | | - </div> |
| 73 | + <span ref={contentCallbackRef}>{children[currentIndex]}</span> |
| 74 | + {nextChild} |
56 | 75 | </div> |
57 | | - ); |
58 | | - } |
59 | | - |
60 | | - componentDidMount() { |
61 | | - const { delay } = this.props; |
62 | | - |
63 | | - this.heightTimeout = setTimeout(() => { |
64 | | - this._calculateContentHeight(); |
65 | | - }, 50); |
66 | | - |
67 | | - this.animationTimeout = setTimeout(() => { |
68 | | - this.setState({ isAnimating: true }); |
69 | | - }, delay); |
70 | | - |
71 | | - window.addEventListener("resize", this._calculateContentHeight); |
72 | | - } |
73 | | - |
74 | | - componentWillUnmount() { |
75 | | - clearTimeout(this.heightTimeout); |
76 | | - clearTimeout(this.animationTimeout); |
77 | | - clearTimeout(this.repeatTimeout); |
78 | | - window.removeEventListener("resize", this._calculateContentHeight); |
79 | | - } |
80 | | - |
81 | | - _calculateContentHeight = () => { |
82 | | - if (this.content) { |
83 | | - this.setState({ |
84 | | - contentHeight: this.content.clientHeight, |
85 | | - }); |
86 | | - } |
87 | | - }; |
| 76 | + </div> |
| 77 | + ); |
| 78 | +} |
88 | 79 |
|
89 | | - _handleTransitionEnd = () => { |
90 | | - const { children, repeatDelay } = this.props; |
| 80 | +TextRotater.propTypes = { |
| 81 | + children: PropTypes.arrayOf(PropTypes.node), |
| 82 | + delay: PropTypes.number, |
| 83 | + repeatDelay: PropTypes.number, |
| 84 | + // Needed to prevent jump when |
| 85 | + // rotating between texts of different widths |
| 86 | + maxWidth: PropTypes.number, |
| 87 | +}; |
91 | 88 |
|
92 | | - this.setState( |
93 | | - { |
94 | | - currentIndex: (this.state.currentIndex + 1) % Children.count(children), |
95 | | - isAnimating: false, |
96 | | - }, |
97 | | - () => { |
98 | | - this.repeatTimeout = setTimeout(() => { |
99 | | - this.setState({ isAnimating: true }); |
100 | | - }, repeatDelay); |
101 | | - }, |
102 | | - ); |
103 | | - }; |
104 | | -} |
| 89 | +export default memo(TextRotater); |
0 commit comments