import { HTMLProps , CSSProperties , ReactNode , ReactElement , MutableRefObject , useRef , useMemo , useCallback , useEffect , useLayoutEffect } from 'react' ; import PropTypes from 'prop-types' ; import { css , cx , keyframes } from '@emotion/css' ; import { jsx } from '@emotion/react' ; import { AnimatorRef , useAnimator } from '@arwes/animator' ; import { useBleeps } from '@arwes/bleeps' ; import { TextAnimationRefs , startTextAnimation } from '../utils/textAnimations' ; import { generateStyles } from './Text.styles' ; import { NoInfer } from '../utils/types' ; const FPS_NORMAL_DURATION = 1000 / 60 ; interface TextProps < E extends HTMLElement = HTMLSpanElement > { as ? : keyof HTMLElementTagNameMap ; blink ? : boolean ; blinkText ? : string ; blinkInterval ? : number ; dynamicDuration ? : boolean ; dynamicDurationFactor ? : number ; className ? : string ; style ? : CSSProperties ; rootRef ? : MutableRefObject < E | null > | ( ( node : E ) => void ) ; children ? : ReactNode ; } const Text = < E extends HTMLElement = HTMLSpanElement , P extends HTMLProps < HTMLElement > = HTMLProps < E > > ( props ) : ReactElement => { const { as : providedAs , blink : hasBlink , blinkText , blinkInterval , dynamicDuration , dynamicDurationFactor , className , style , rootRef : providedRootRef , children , ... otherProps } = props ; const internalRootRef = useRef < E | null > ( null ) ; const actualChildrenRef = useRef < HTMLSpanElement | null > ( null ) ; const cloneNode = useRef < HTMLSpanElement | null > ( null ) ; const blinkNode = useRef < HTMLSpanElement | null > ( null ) ; const animationFrame = useRef < number | null > ( null ) ; const animateRefs = useRef ( { rootRef : internalRootRef , actualChildrenRef , cloneNode , blinkNode , animationFrame } ) ; const animator = useAnimator ( ) as AnimatorRef ; const bleeps = useBleeps ( ) ; const styles = useMemo ( ( ) => generateStyles ( { animate : animator . animate } ) , [ animator . animate ] ) ; const as = useMemo ( ( ) => providedAs , [ ] ) ; const rootRef = useCallback ( ( node ) => { internalRootRef . current = node ; if ( typeof providedRootRef === 'function' ) { providedRootRef ( node ) ; } else if ( providedRootRef ) { providedRootRef . current = node ; } } , [ ] ) ; useEffect ( ( ) => { if ( animator . animate && hasBlink && blinkText && blinkInterval ) { const blinkKeyframes = keyframes ( styles . blinkKeyframes ) ; const blinkClassName = css ( { ... styles . blink , animation : `${ blinkKeyframes } ${ blinkInterval }ms step-end infinite` } ) ; blinkNode . current = document . createElement ( 'span' ) ; blinkNode . current . innerHTML = blinkText ; blinkNode . current . setAttribute ( 'class' , blinkClassName ) ; } else { blinkNode . current = null ; } } , [ animator . animate , hasBlink ] ) ; const childrenContent = useRef < string > ( '' ) ; useLayoutEffect ( ( ) => { if ( ! animator . animate ) { return ; } if ( dynamicDuration ) { const newChildrenTextContent = String ( actualChildrenRef . current ?. textContent || '' ) ; const factor = dynamicDurationFactor || FPS_NORMAL_DURATION ; const newDynamicDuration = Math . min ( factor * newChildrenTextContent . length , animator . duration . enter ) ; if ( animator . duration . enter !== newDynamicDuration ) { animator . updateDuration ( { enter : newDynamicDuration } ) ; } } const newChildrenContent = String ( actualChildrenRef . current ?. innerHTML || '' ) ; const isChildrenContentEqual = newChildrenContent === childrenContent . current ; childrenContent . current = newChildrenContent ; if ( animator . flow . entered && ! isChildrenContentEqual ) { bleeps . type ?. play ( ) ; startTextAnimation ( animator , animateRefs , ( ) => { bleeps . type ?. stop ( ) ; } ) ; } } , [ children ] ) ; animator . setupAnimateRefs ( animateRefs , bleeps ) ; return jsx ( as as string , { ... otherProps , className : cx ( 'arwes-text' , className ) , css : styles . root , style , ref : rootRef } , jsx ( 'span' , { ref : actualChildrenRef , css : styles . actualChildren , className : 'arwes-text__content' } , children ) ) ; } ; Text . propTypes = { as : PropTypes . string , dynamicDuration : PropTypes . bool , dynamicDurationFactor : PropTypes . number , blink : PropTypes . bool , blinkText : PropTypes . string , blinkInterval : PropTypes . number , rootRef : PropTypes . any } ; Text . defaultProps = { as : 'span' , dynamicDuration : true , dynamicDurationFactor : FPS_NORMAL_DURATION , blink : true , blinkText : '&#9614;' , blinkInterval : 100 } ; export { TextProps , Text } ;