From d45713497660aad9f0aa21c169e3900c9e67da8b Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Sun, 4 Sep 2022 17:32:04 -0400 Subject: [PATCH 1/5] Start converting the TextLoop to a function component, to use hooks. --- package.json | 8 +- src/components/TextLoop.tsx | 169 +++++++++++++++++++++++++++++++++++- src/utils.ts | 20 +++++ yarn.lock | 32 +++---- 4 files changed, 206 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index d4931f8..ca578c6 100644 --- a/package.json +++ b/package.json @@ -83,8 +83,8 @@ "npm-run-all": "^4.1.5", "parcel-bundler": "^1.12.4", "prettier": "^1.19.1", - "react": "^16.12.0", - "react-dom": "^16.12.0", + "react": "16.13.1", + "react-dom": "16.13.1", "rimraf": "^3.0.0", "source-map-loader": "^0.2.4", "ts-jest": "^24.2.0", @@ -93,8 +93,8 @@ "webpack-cli": "^3.3.10" }, "peerDependencies": { - "react": "^15.0.1 || ^16.0.1", - "react-dom": "^15.0.1 || ^16.0.1" + "react": "^16.8", + "react-dom": "^16.8" }, "dependencies": { "cxs": "^6.2.0", diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index 51866ba..dd71ec8 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState, useRef } from "react"; import { TransitionMotion, spring, @@ -7,7 +7,12 @@ import { } from "react-motion"; import cxs from "cxs"; import isEqual from "react-fast-compare"; -import { requestTimeout, clearRequestTimeout, RequestTimeout } from "../utils"; +import { + useRequestTimeout, + requestTimeout, + clearRequestTimeout, + RequestTimeout, +} from "../utils"; type Props = { children?: (JSX.Element | string)[]; @@ -33,7 +38,165 @@ type State = { currentInterval: number; }; -class TextLoop extends React.PureComponent { +function TextLoop({ + children, + interval = 3000, + delay = 0, + adjustingSpeed = 150, + springConfig = { stiffness: 340, damping: 30 }, + fade = true, + mask = false, + noWrap = true, + className = "", + onChange, +}: Props) { + const elements = React.Children.toArray(children); // TODO: useMemo this? + + const [currentEl, setCurrentEl] = useState(elements[0]); + const [currentWordIndex, setCurrentWordIndex] = useState(0); + const [wordCount, setWordCount] = useState(0); + const [usedDelay, setUsedDelay] = useState(false); + const wordBoxRef = useRef(null); + + const currentInterval = Array.isArray(interval) + ? interval[currentWordIndex % interval.length] + : interval; + let nextTickInterval: number | null = usedDelay + ? currentInterval + : currentInterval + delay; + if (!(currentInterval > 0 && React.Children.count(children) > 1)) { + nextTickInterval = null; // Disable the timeout + } + + // TODO: componentDidUpdate includes some code for currentInterval == 0 I think + function tick() { + setUsedDelay(true); // Next tick won't have a delay + + const newWordIndex = (currentWordIndex + 1) % elements.length; + setCurrentWordIndex(newWordIndex); + const nextEl = elements[newWordIndex]; + setCurrentEl(nextEl); + + setWordCount((wordCount + 1) % 1000); // just a safe value to avoid infinite counts + + if (onChange) { + onChange(); // TODO: This used to take the state + } + } + useRequestTimeout(tick, nextTickInterval, wordCount); + + function getDimensions() { + if (wordBoxRef.current == null) { + return { + width: 0, + height: 0, + }; + } + return wordBoxRef.current.getBoundingClientRect(); + } + + function getOpacity(): 0 | 1 { + return fade ? 0 : 1; + } + + function willLeave(): { opacity: OpaqueConfig; translate: OpaqueConfig } { + const { height } = getDimensions(); + + return { + opacity: spring(getOpacity(), springConfig), + translate: spring(-height, springConfig), + }; + } + + // Fade in animation + function willEnter(): { opacity: 0 | 1; translate: number } { + const { height } = getDimensions(); + + return { + opacity: getOpacity(), + translate: height, + }; + } + + const wrapperStyles = cxs({ + ...(mask && { overflow: "hidden" }), + ...{ + display: "inline-block", + position: "relative", + verticalAlign: "top", + }, + }); + + const elementStyles = cxs({ + display: "inline-block", + left: 0, + top: 0, + whiteSpace: noWrap ? "nowrap" : "normal", + }); + + const transitionMotionStyles = [ + { + key: `step-${wordCount}`, + data: { + currentEl, + }, + style: { + opacity: spring(1, springConfig), + translate: spring(0, springConfig), + }, + }, + ]; + + return ( +
+ + {(interpolatedStyles): JSX.Element => { + const { height, width } = getDimensions(); + + const parsedWidth = + wordBoxRef.current == null ? "auto" : width; + + const parsedHeight = + wordBoxRef.current == null ? "auto" : height; + + return ( +
+ {interpolatedStyles.map(config => ( +
+ {config.data.currentEl} +
+ ))} +
+ ); + }} +
+
+ ); +} + +export class TextLoopOld extends React.PureComponent { isUnMounting = false; tickDelay: RequestTimeout = 0; diff --git a/src/utils.ts b/src/utils.ts index 2f39e06..6598c86 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { useEffect, useRef } from "react"; + declare global { interface Window { mozRequestAnimationFrame; @@ -97,3 +99,21 @@ export const clearRequestTimeout = function(handle): void { ? window.msCancelRequestAnimationFrame(handle.value) : clearTimeout(handle); }; + +export function useRequestTimeout(callback: () => void, delay: number | null, identifier: any) { + const savedCallback = useRef(callback) + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + if (!delay && delay !== 0) { + return + } + + const handle = requestTimeout(() => savedCallback.current(), delay) + + return () => clearRequestTimeout(handle) + }, [delay, identifier]) +} + diff --git a/yarn.lock b/yarn.lock index 381a6a6..e2f4cf8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7447,15 +7447,15 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.12.0.tgz#0da4b714b8d13c2038c9396b54a92baea633fe11" - integrity sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw== +react-dom@16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" + integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" prop-types "^15.6.2" - scheduler "^0.18.0" + scheduler "^0.19.1" react-fast-compare@2.0.4: version "2.0.4" @@ -7476,10 +7476,10 @@ react-motion@^0.5.2: prop-types "^15.5.8" raf "^3.1.0" -react@^16.12.0: - version "16.12.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.12.0.tgz#0c0a9c6a142429e3614834d5a778e18aa78a0b83" - integrity sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA== +react@16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" + integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -7908,10 +7908,10 @@ saxes@^3.1.9: dependencies: xmlchars "^2.1.1" -scheduler@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4" - integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ== +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -8751,9 +8751,9 @@ typedarray@^0.0.6: integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= typescript@^3.7.3: - version "3.7.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" - integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== + version "3.9.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" + integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q== uglify-js@^3.1.4: version "3.6.9" From d6598ca65148b143c05916ee8d4af08356773571 Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Sun, 4 Sep 2022 17:50:55 -0400 Subject: [PATCH 2/5] Remove old code. --- src/components/TextLoop.tsx | 288 +----------------------------------- 1 file changed, 8 insertions(+), 280 deletions(-) diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index dd71ec8..d6b0a7d 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -3,41 +3,28 @@ import { TransitionMotion, spring, OpaqueConfig, - TransitionStyle, } from "react-motion"; import cxs from "cxs"; -import isEqual from "react-fast-compare"; import { useRequestTimeout, - requestTimeout, - clearRequestTimeout, - RequestTimeout, } from "../utils"; type Props = { children?: (JSX.Element | string)[]; - interval: number | number[]; - delay: number; - adjustingSpeed: number; - springConfig: { + interval?: number | number[]; + delay?: number; + adjustingSpeed?: number; + springConfig?: { stiffness: number; damping: number; }; - fade: boolean; - mask: boolean; - noWrap: boolean; + fade?: boolean; + mask?: boolean; + noWrap?: boolean; className?: string; onChange?: Function; }; -type State = { - elements: (JSX.Element | string | undefined)[]; - currentEl: JSX.Element | string | undefined; - currentWordIndex: number; - wordCount: number; - currentInterval: number; -}; - function TextLoop({ children, interval = 3000, @@ -72,7 +59,7 @@ function TextLoop({ function tick() { setUsedDelay(true); // Next tick won't have a delay - const newWordIndex = (currentWordIndex + 1) % elements.length; + const newWordIndex = (currentWordIndex + 1) % (children?.length ?? 0); setCurrentWordIndex(newWordIndex); const nextEl = elements[newWordIndex]; setCurrentEl(nextEl); @@ -196,263 +183,4 @@ function TextLoop({ ); } -export class TextLoopOld extends React.PureComponent { - isUnMounting = false; - - tickDelay: RequestTimeout = 0; - - tickLoop: RequestTimeout = 0; - - wordBox: HTMLDivElement | null = null; - - static defaultProps: Props = { - interval: 3000, - delay: 0, - adjustingSpeed: 150, - springConfig: { stiffness: 340, damping: 30 }, - fade: true, - mask: false, - noWrap: true, - }; - - constructor(props: Props) { - super(props); - const elements = React.Children.toArray(props.children); - - this.state = { - elements, - currentEl: elements[0], - currentWordIndex: 0, - wordCount: 0, - currentInterval: Array.isArray(props.interval) - ? props.interval[0] - : props.interval, - }; - } - - componentDidMount(): void { - // Starts animation - const { delay } = this.props; - const { currentInterval, elements } = this.state; - - if (currentInterval > 0 && elements.length > 1) { - this.tickDelay = requestTimeout(() => { - this.tickLoop = requestTimeout(this.tick, currentInterval); - }, delay); - } - } - - componentDidUpdate(prevProps: Props, prevState: State): void { - const { interval, children, delay } = this.props as Props; - const { currentWordIndex } = this.state; - - const currentInterval = Array.isArray(interval) - ? interval[currentWordIndex % interval.length] - : interval; - - if (prevState.currentInterval !== currentInterval) { - this.clearTimeouts(); - - if (currentInterval > 0 && React.Children.count(children) > 1) { - this.tickDelay = requestTimeout(() => { - this.tickLoop = requestTimeout(this.tick, currentInterval); - }, delay); - } else { - this.setState((state, props) => { - const { currentWordIndex: _currentWordIndex } = state; - - return { - currentInterval: Array.isArray(props.interval) - ? props.interval[ - _currentWordIndex % props.interval.length - ] - : props.interval, - }; - }); - } - } - - if (!isEqual(prevProps.children, children)) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - elements: React.Children.toArray(children), - }); - } - } - - componentWillUnmount(): void { - this.isUnMounting = true; - this.clearTimeouts(); - } - - clearTimeouts(): void { - if (this.tickLoop != null) { - clearRequestTimeout(this.tickLoop); - } - - if (this.tickDelay != null) { - clearRequestTimeout(this.tickDelay); - } - } - - // Fade out animation - willLeave = (): { opacity: OpaqueConfig; translate: OpaqueConfig } => { - const { height } = this.getDimensions(); - - return { - opacity: spring(this.getOpacity(), this.props.springConfig), - translate: spring(-height, this.props.springConfig), - }; - }; - - // Fade in animation - willEnter = (): { opacity: 0 | 1; translate: number } => { - const { height } = this.getDimensions(); - - return { - opacity: this.getOpacity(), - translate: height, - }; - }; - - tick = (): void => { - if (!this.isUnMounting) { - this.setState( - (state, props) => { - const currentWordIndex = - (state.currentWordIndex + 1) % state.elements.length; - - const currentEl = state.elements[currentWordIndex]; - const updatedState = { - currentWordIndex, - currentEl, - wordCount: (state.wordCount + 1) % 1000, // just a safe value to avoid infinite counts, - currentInterval: Array.isArray(props.interval) - ? props.interval[ - currentWordIndex % props.interval.length - ] - : props.interval, - }; - if (props.onChange) { - props.onChange(updatedState); - } - - return updatedState; - }, - () => { - if (this.state.currentInterval > 0) { - this.clearTimeouts(); - this.tickLoop = requestTimeout( - this.tick, - this.state.currentInterval - ); - } - } - ); - } - }; - - getOpacity(): 0 | 1 { - return this.props.fade ? 0 : 1; - } - - getDimensions(): ClientRect | DOMRect | { width: 0; height: 0 } { - if (this.wordBox == null) { - return { - width: 0, - height: 0, - }; - } - - return this.wordBox.getBoundingClientRect(); - } - - wrapperStyles = cxs({ - ...(this.props.mask && { overflow: "hidden" }), - ...{ - display: "inline-block", - position: "relative", - verticalAlign: "top", - }, - }); - - elementStyles = cxs({ - display: "inline-block", - left: 0, - top: 0, - whiteSpace: this.props.noWrap ? "nowrap" : "normal", - }); - - getTransitionMotionStyles(): TransitionStyle[] { - const { springConfig } = this.props; - const { wordCount, currentEl } = this.state; - - return [ - { - key: `step-${wordCount}`, - data: { - currentEl, - }, - style: { - opacity: spring(1, springConfig), - translate: spring(0, springConfig), - }, - }, - ]; - } - - render(): JSX.Element { - const { className = "" } = this.props; - return ( -
- - {(interpolatedStyles): JSX.Element => { - const { height, width } = this.getDimensions(); - - const parsedWidth = - this.wordBox == null ? "auto" : width; - - const parsedHeight = - this.wordBox == null ? "auto" : height; - - return ( -
- {interpolatedStyles.map(config => ( -
{ - this.wordBox = n; - }} - key={config.key} - style={{ - opacity: config.style.opacity, - transform: `translateY(${config.style.translate}px)`, - position: - this.wordBox == null - ? "relative" - : "absolute", - }} - > - {config.data.currentEl} -
- ))} -
- ); - }} -
-
- ); - } -} - export default TextLoop; From df49b39ebe759357853d6cd01622745065150cfb Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Sun, 4 Sep 2022 20:37:21 -0400 Subject: [PATCH 3/5] Use react-spring instead of react-motion. --- package.json | 2 +- src/components/TextLoop.tsx | 333 +++++++++++++++++------------------- yarn.lock | 119 ++++++++++--- 3 files changed, 257 insertions(+), 197 deletions(-) diff --git a/package.json b/package.json index ca578c6..e498e0e 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,6 @@ "dependencies": { "cxs": "^6.2.0", "react-fast-compare": "2.0.4", - "react-motion": "^0.5.2" + "react-spring": "^9.5.2" } } diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index d6b0a7d..3493731 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -1,186 +1,169 @@ -import React, { useState, useRef } from "react"; -import { - TransitionMotion, - spring, - OpaqueConfig, -} from "react-motion"; -import cxs from "cxs"; -import { - useRequestTimeout, -} from "../utils"; +import React, { useState, useRef, useEffect } from 'react' +import { animated, useTransition } from 'react-spring' +import cxs from 'cxs' +import { useRequestTimeout } from '../utils' type Props = { - children?: (JSX.Element | string)[]; - interval?: number | number[]; - delay?: number; - adjustingSpeed?: number; - springConfig?: { - stiffness: number; - damping: number; - }; - fade?: boolean; - mask?: boolean; - noWrap?: boolean; - className?: string; - onChange?: Function; -}; + children?: (JSX.Element | string)[] + interval?: number | number[] + delay?: number + adjustingSpeed?: number + springConfig?: { + stiffness: number + damping: number + } + fade?: boolean + mask?: boolean + noWrap?: boolean + className?: string + onChange?: Function +} function TextLoop({ - children, - interval = 3000, - delay = 0, - adjustingSpeed = 150, - springConfig = { stiffness: 340, damping: 30 }, - fade = true, - mask = false, - noWrap = true, - className = "", - onChange, + children, + interval = 3000, + delay = 0, + adjustingSpeed = 150, + springConfig = { stiffness: 340, damping: 30 }, + fade = true, + mask = false, + noWrap = true, + className = '', + onChange, }: Props) { - const elements = React.Children.toArray(children); // TODO: useMemo this? - - const [currentEl, setCurrentEl] = useState(elements[0]); - const [currentWordIndex, setCurrentWordIndex] = useState(0); - const [wordCount, setWordCount] = useState(0); - const [usedDelay, setUsedDelay] = useState(false); - const wordBoxRef = useRef(null); - - const currentInterval = Array.isArray(interval) - ? interval[currentWordIndex % interval.length] - : interval; - let nextTickInterval: number | null = usedDelay - ? currentInterval - : currentInterval + delay; - if (!(currentInterval > 0 && React.Children.count(children) > 1)) { - nextTickInterval = null; // Disable the timeout + const elements = React.Children.toArray(children) // TODO: useMemo this? + + const [state, setState] = useState({ + currentEl: elements[0], + currentWordIndex: 0, + wordCount: 0, + usedDelay: false, + }) + const wordBoxRef = useRef(null) + // We set this state each time we get a new wordBoxRef in order to re-render immediately + const [_, setWordBoxRefKey] = useState(null) + + const currentInterval = Array.isArray(interval) + ? interval[state.currentWordIndex % interval.length] + : interval + + const currentItem = { + el: state.currentEl, + key: `step-${state.wordCount}`, + } + const transitions = useTransition([currentItem], { + from: () => { + // Fade in animation + const { height } = getDimensions() + return { + opacity: getOpacity(), + translate: height, + } + }, + enter: { + // At rest values + opacity: 1, + translate: 0, + }, + leave: () => { + // Fade out animation + const { height } = getDimensions() + return { + opacity: getOpacity(), + translate: -height, + } + }, + config: springConfig, + keys: (item) => item.key, + }) + + let nextTickInterval: number | null = state.usedDelay ? currentInterval : currentInterval + delay + const needsTick = currentInterval > 0 && React.Children.count(children) > 1 + if (!needsTick) { + nextTickInterval = null // Disable the timeout + } + + // TODO: componentDidUpdate includes some code for currentInterval == 0 I think + function tick() { + const newWordIndex = (state.currentWordIndex + 1) % (children?.length ?? 0) + const nextEl = elements[newWordIndex] + + const newState = { + usedDelay: true, // Next tick won't have a delay + currentWordIndex: newWordIndex, + currentEl: nextEl, + wordCount: (state.wordCount + 1) % 1000, // just a safe value to avoid infinite counts } + setState(newState) - // TODO: componentDidUpdate includes some code for currentInterval == 0 I think - function tick() { - setUsedDelay(true); // Next tick won't have a delay - - const newWordIndex = (currentWordIndex + 1) % (children?.length ?? 0); - setCurrentWordIndex(newWordIndex); - const nextEl = elements[newWordIndex]; - setCurrentEl(nextEl); - - setWordCount((wordCount + 1) % 1000); // just a safe value to avoid infinite counts - - if (onChange) { - onChange(); // TODO: This used to take the state - } + if (onChange) { + onChange(newState) // TODO: This used to be a different format } - useRequestTimeout(tick, nextTickInterval, wordCount); - - function getDimensions() { - if (wordBoxRef.current == null) { - return { - width: 0, - height: 0, - }; - } - return wordBoxRef.current.getBoundingClientRect(); + } + useRequestTimeout(tick, nextTickInterval, state.wordCount) + + function getDimensions() { + if (wordBoxRef.current == null) { + return { + width: 0, + height: 0, + } } - - function getOpacity(): 0 | 1 { - return fade ? 0 : 1; - } - - function willLeave(): { opacity: OpaqueConfig; translate: OpaqueConfig } { - const { height } = getDimensions(); - - return { - opacity: spring(getOpacity(), springConfig), - translate: spring(-height, springConfig), - }; - } - - // Fade in animation - function willEnter(): { opacity: 0 | 1; translate: number } { - const { height } = getDimensions(); - - return { - opacity: getOpacity(), - translate: height, - }; - } - - const wrapperStyles = cxs({ - ...(mask && { overflow: "hidden" }), - ...{ - display: "inline-block", - position: "relative", - verticalAlign: "top", - }, - }); - - const elementStyles = cxs({ - display: "inline-block", - left: 0, - top: 0, - whiteSpace: noWrap ? "nowrap" : "normal", - }); - - const transitionMotionStyles = [ - { - key: `step-${wordCount}`, - data: { - currentEl, - }, - style: { - opacity: spring(1, springConfig), - translate: spring(0, springConfig), - }, - }, - ]; - - return ( -
- - {(interpolatedStyles): JSX.Element => { - const { height, width } = getDimensions(); - - const parsedWidth = - wordBoxRef.current == null ? "auto" : width; - - const parsedHeight = - wordBoxRef.current == null ? "auto" : height; - - return ( -
- {interpolatedStyles.map(config => ( -
- {config.data.currentEl} -
- ))} -
- ); - }} -
-
- ); + return wordBoxRef.current.getBoundingClientRect() + } + + function getOpacity(): 0 | 1 { + return fade ? 0 : 1 + } + + const wrapperStyles = cxs({ + ...(mask && { overflow: 'hidden' }), + ...{ + display: 'inline-block', + position: 'relative', + verticalAlign: 'top', + }, + }) + + const elementStyles = cxs({ + display: 'inline-block', + left: 0, + top: 0, + whiteSpace: noWrap ? 'nowrap' : 'normal', + }) + + const { height, width } = getDimensions() + const parsedWidth = wordBoxRef.current == null ? 'auto' : width + const parsedHeight = wordBoxRef.current == null ? 'auto' : height + + return ( +
+
+ {transitions(({ opacity, translate }, item) => ( + { + if (n && item.key === currentItem.key) { + wordBoxRef.current = n + setWordBoxRefKey(item.key) + } + }} + style={{ + opacity: opacity, + transform: translate.to((y) => `translateY(${y}px)`), + position: wordBoxRef.current === null ? 'relative' : 'absolute', + }}> + {item.el} + + ))} +
+
+ ) } -export default TextLoop; +export default TextLoop diff --git a/yarn.lock b/yarn.lock index e2f4cf8..a583c6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1656,6 +1656,92 @@ "@parcel/utils" "^1.11.0" physical-cpu-count "^2.0.0" +"@react-spring/animated@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/animated/-/animated-9.5.2.tgz#42785b4f369d9715e9ee32c04b78483e7bb85489" + integrity sha512-oRlX+MmYLbK8IuUZR7SQUnRjXxJ4PMIZeBkBd1SUWVgVJAHMTfJzPltzm+I6p59qX+qLlklYHfnWaonQKDqLuQ== + dependencies: + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/core@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/core/-/core-9.5.2.tgz#c8450783ce87a82d3f9ab21e2650e42922398ff7" + integrity sha512-UMRtFH6EfebMp/NMDGCUY5+hZFXsg9iT9hzt/iPzJSz2WMXKBjLoFZHJXcmiVOrIhzHmg1O0pFECn1Wp6pZ5Gw== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/rafz" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/konva@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/konva/-/konva-9.5.2.tgz#cbc7c75c55c7946481f86c7392a6656bb5b1bf4a" + integrity sha512-FN8LpbGQtm2pllU9mOyYjYwvLtA9EiIPWk2NVuhhX+5lJZrdCWuEY7EyFpK8PtgZXBdVj8bj7eIu1LlTnARW/A== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/core" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/native@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/native/-/native-9.5.2.tgz#218fa228a746cb2f535ea59b317d2e99cdfed02d" + integrity sha512-G9BCAKVADLweLR43uyMnTrOnYDb4BboYvqKY+0X1fLs45PNrfbBXnSLot4g+5x3HjblypJgNq7CjHlqZKI980g== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/core" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/rafz@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/rafz/-/rafz-9.5.2.tgz#1264d5df09717cf46d55055da2c55ff84f59073f" + integrity sha512-xHSRXKKBI/wDUkZGrspkOm4VlgN6lZi8Tw9Jzibp9QKf3neoof+U2mDNgklvnLaasymtUwAq9o4ZfFvQIVNgPQ== + +"@react-spring/shared@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/shared/-/shared-9.5.2.tgz#e0a252e06daa3927964460fef05d8092e7d78ffc" + integrity sha512-/OSf2sjwY4BUnjZL6xMC+H3WxOOhMUCk+yZwgdj40XuyUpk6E6tYyiPeD9Yq5GLsZHodkvE1syVMRVReL4ndAg== + dependencies: + "@react-spring/rafz" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/three@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/three/-/three-9.5.2.tgz#965ff4e729929ebbb9a1f8e84f3f4acb6acec4f9" + integrity sha512-3H7Lv8BJZ3dajh0yJA3m9rEbqz5ZNrTCAkhVOeLqgvBlcWU5qVs4luYA1Z7H4vZnLqVtzv+kHAyg3XIpuTOXhQ== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/core" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/types@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/types/-/types-9.5.2.tgz#cce1b03afbafb23edfb9cd8c517cc7462abffb65" + integrity sha512-n/wBRSHPqTmEd4BFWY6TeR1o/UY+3ujoqMxLjqy90CcY/ozJzDRuREL3c+pxMeTF2+B7dX33dTPCtFMX51nbxg== + +"@react-spring/web@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/web/-/web-9.5.2.tgz#762ee6b3c8fea40281e1298f5cf1c0515ad6a794" + integrity sha512-cusTjbOGTgtbsnpBDjb6Ia+B0lQLE0Fk5rGDog6Sww7hWnLIQ521PMiOBnAWtkntB9eXDUfj7L91nwJviEC0lw== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/core" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + +"@react-spring/zdog@~9.5.2": + version "9.5.2" + resolved "https://registry.yarnpkg.com/@react-spring/zdog/-/zdog-9.5.2.tgz#a3e451378c23caa4381b5821d3d52c3017740c55" + integrity sha512-zUX8RzX8gM51g8NJ5Qaf15KNKQgN3qN/8m5FvqmiqZ5ZGqjoHkbCoMD3o2MICTUN1l+d4eUu9TYrmiO2bgJo/g== + dependencies: + "@react-spring/animated" "~9.5.2" + "@react-spring/core" "~9.5.2" + "@react-spring/shared" "~9.5.2" + "@react-spring/types" "~9.5.2" + "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.2" resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz#8013f2af54a2b7d735f71560ff360d3a8176a87b" @@ -6808,11 +6894,6 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -performance-now@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" - integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU= - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -7300,7 +7381,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.3" -prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -7410,13 +7491,6 @@ quote-stream@^1.0.1, quote-stream@~1.0.2: minimist "^1.1.3" through2 "^2.0.0" -raf@^3.1.0: - version "3.4.1" - resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" - integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== - dependencies: - performance-now "^2.1.0" - randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -7467,14 +7541,17 @@ react-is@^16.8.1, react-is@^16.8.4: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== -react-motion@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316" - integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ== - dependencies: - performance-now "^0.2.0" - prop-types "^15.5.8" - raf "^3.1.0" +react-spring@^9.5.2: + version "9.5.2" + resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-9.5.2.tgz#b9929ad2806e56e6408b27189ec9cdf1dc003873" + integrity sha512-OGWNgKi2TSjpqsK67NCUspaCgEvWcG7HcpO9KAaDLFzFGNxWdGdN3YTXhhWUqCsLAx9I6LxPzmRuUPsMNqTgrw== + dependencies: + "@react-spring/core" "~9.5.2" + "@react-spring/konva" "~9.5.2" + "@react-spring/native" "~9.5.2" + "@react-spring/three" "~9.5.2" + "@react-spring/web" "~9.5.2" + "@react-spring/zdog" "~9.5.2" react@16.13.1: version "16.13.1" From 8264e38afa4a6ee0a014d73b1fcdc32a35720407 Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Sun, 4 Sep 2022 21:12:44 -0400 Subject: [PATCH 4/5] Memoize the call to React.children.toArray. --- src/components/TextLoop.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index 3493731..b1e6981 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react' +import React, { useState, useRef, useMemo } from 'react' import { animated, useTransition } from 'react-spring' import cxs from 'cxs' import { useRequestTimeout } from '../utils' @@ -31,7 +31,7 @@ function TextLoop({ className = '', onChange, }: Props) { - const elements = React.Children.toArray(children) // TODO: useMemo this? + const elements = useMemo(() => React.Children.toArray(children), [children]) const [state, setState] = useState({ currentEl: elements[0], From 234ab5a014440d09ce0e378d88365c0ac47cf6ad Mon Sep 17 00:00:00 2001 From: Bridger Maxwell Date: Sun, 4 Sep 2022 21:20:07 -0400 Subject: [PATCH 5/5] Fix build issues --- src/components/TextLoop.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextLoop.tsx b/src/components/TextLoop.tsx index b1e6981..2d1e7e7 100644 --- a/src/components/TextLoop.tsx +++ b/src/components/TextLoop.tsx @@ -41,7 +41,7 @@ function TextLoop({ }) const wordBoxRef = useRef(null) // We set this state each time we get a new wordBoxRef in order to re-render immediately - const [_, setWordBoxRefKey] = useState(null) + const [, setWordBoxRefKey] = useState(null) const currentInterval = Array.isArray(interval) ? interval[state.currentWordIndex % interval.length] @@ -85,7 +85,7 @@ function TextLoop({ // TODO: componentDidUpdate includes some code for currentInterval == 0 I think function tick() { - const newWordIndex = (state.currentWordIndex + 1) % (children?.length ?? 0) + const newWordIndex = (state.currentWordIndex + 1) % elements.length const nextEl = elements[newWordIndex] const newState = {