-
Notifications
You must be signed in to change notification settings - Fork 0
Updated the fade-in effect on reveal of enhanced text and fix other issues on feedback #83
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
Changes from 4 commits
2a78f5a
25fd7d4
a806363
54b30fa
93abf97
4f0739b
da29cc8
4be85cd
fbfc2df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,36 @@ | ||
| import React from 'react' | ||
| import classnames from 'classnames' | ||
| import PropTypes from 'prop-types' | ||
| import classnames from 'classnames' | ||
|
|
||
| function PromptInput ({ className, children }) { | ||
|
|
||
| let textarea = null | ||
| let actions = null | ||
| let overlay = null | ||
|
|
||
| React.Children.forEach(children, child => { | ||
| if (child.type && child.type.displayName === 'PromptInputTextarea') { | ||
|
||
| textarea = child | ||
| } else if (child.type && child.type.displayName === 'PromptInputActions') { | ||
| actions = child | ||
| } else if (child.type && child.type.name === 'TextRevealOverlay') { | ||
| overlay = child | ||
| } | ||
| }) | ||
|
|
||
| function PromptInput ({ children, className }) { | ||
| return ( | ||
| <div className={classnames('ds-prompt-input', className)}> | ||
| {children} | ||
| {textarea} | ||
| {overlay} | ||
| {actions} | ||
| {React.Children.map(children, child => { | ||
|
||
| if ((child.type && child.type.displayName !== 'PromptInputTextarea' && | ||
| child.type.displayName !== 'PromptInputActions') && | ||
| (child.type && child.type.name !== 'TextRevealOverlay')) { | ||
| return child | ||
| } | ||
| return null | ||
| })} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
@@ -15,10 +40,5 @@ PromptInput.propTypes = { | |
| children: PropTypes.node | ||
| } | ||
|
|
||
| PromptInput.defaultProps = { | ||
| className: '', | ||
| children: null | ||
| } | ||
|
|
||
| export default PromptInput | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import React, { useEffect, useState, useRef } from 'react' | ||
| import PropTypes from 'prop-types' | ||
|
|
||
| function TextRevealOverlay({ | ||
| text, | ||
| isRevealing, | ||
| lastParagraphIndex, | ||
| className, | ||
| textareaRef, | ||
| onAnimationComplete | ||
| }) { | ||
| const [paragraphs, setParagraphs] = useState([]) | ||
| const [isAnimatingLastParagraph, setIsAnimatingLastParagraph] = useState(false) | ||
| const overlayRef = useRef(null) | ||
|
|
||
| // Split text into paragraphs whenever text changes | ||
| useEffect(() => { | ||
| if (text) { | ||
| const split = text.split(/\n{2,}|\r\n{2,}/) | ||
zhadaev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .map(p => p.trim()) | ||
| .filter(p => p.length > 0) | ||
| setParagraphs(split) | ||
| } else { | ||
| setParagraphs([]) | ||
| } | ||
| }, [text]) | ||
|
|
||
| useEffect(() => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add some short comments to all useEffect hooks in this component?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it would be good to move all those useEffects to a separate hook?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added some comments to the useEffect but did not move into a separate hook since it's only being used by this component. |
||
| if (isAnimatingLastParagraph && lastParagraphIndex === paragraphs.length - 1) { | ||
| const timer = setTimeout(() => { | ||
| setIsAnimatingLastParagraph(false) | ||
| if (onAnimationComplete) { | ||
| onAnimationComplete() | ||
| } | ||
| }, 500) | ||
|
|
||
| return () => clearTimeout(timer) | ||
| } | ||
| }, [isAnimatingLastParagraph, lastParagraphIndex, paragraphs.length, onAnimationComplete]) | ||
|
|
||
|
|
||
| useEffect(() => { | ||
| if (lastParagraphIndex === paragraphs.length - 1 && paragraphs.length > 0) { | ||
| setIsAnimatingLastParagraph(true) | ||
| } | ||
| }, [lastParagraphIndex, paragraphs.length]) | ||
|
|
||
|
|
||
| useEffect(() => { | ||
| if (!textareaRef || !textareaRef.current || !overlayRef.current || !isRevealing) return | ||
|
|
||
| const textarea = textareaRef.current | ||
| const overlay = overlayRef.current | ||
|
|
||
| // Function to update dimensions whenever they change | ||
| const updateDimensions = () => { | ||
| // Position relative to the prompt-input container | ||
| const promptInput = textarea.closest('.ds-prompt-input') | ||
| let paddingLeft = '16px' | ||
| let paddingTop = '16px' | ||
|
|
||
| if (promptInput) { | ||
| const promptStyles = window.getComputedStyle(promptInput) | ||
| if (promptStyles.paddingLeft) { | ||
| paddingLeft = promptStyles.paddingLeft | ||
| } | ||
| if (promptStyles.paddingTop) { | ||
| paddingTop = promptStyles.paddingTop | ||
| } | ||
| } | ||
|
|
||
| // Get current styles | ||
| const textareaStyles = window.getComputedStyle(textarea) | ||
|
|
||
| // Apply exact dimensions and styling | ||
| overlay.style.width = textareaStyles.width | ||
| overlay.style.fontSize = textareaStyles.fontSize | ||
| overlay.style.lineHeight = textareaStyles.lineHeight | ||
| overlay.style.fontFamily = textareaStyles.fontFamily | ||
| overlay.style.fontWeight = textareaStyles.fontWeight | ||
| overlay.style.color = textareaStyles.color | ||
|
|
||
| // Set exact position | ||
| overlay.style.position = 'absolute' | ||
| overlay.style.top = paddingTop | ||
| overlay.style.left = paddingLeft | ||
| overlay.style.width = 'calc(100% - ' + (parseInt(paddingLeft) * 2) + 'px)' | ||
|
||
|
|
||
| // Set max-height to match textarea max-height | ||
| overlay.style.maxHeight = textareaStyles.maxHeight | ||
|
|
||
| // Match the actual height if it's calculated | ||
| if (textarea.style.height) { | ||
| overlay.style.height = textarea.style.height | ||
| } | ||
| } | ||
|
|
||
| // Initial update | ||
| updateDimensions() | ||
|
|
||
| // Set up mutation observer to detect height changes | ||
| let observer = null | ||
| if (typeof MutationObserver !== 'undefined') { | ||
zhadaev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| observer = new MutationObserver((mutations) => { | ||
zhadaev marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| mutations.forEach((mutation) => { | ||
| if (mutation.attributeName === 'style') { | ||
| updateDimensions() | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
| observer.observe(textarea, { | ||
| attributes: true, | ||
| attributeFilter: ['style'] | ||
| }) | ||
| } | ||
|
|
||
| const syncScroll = () => { | ||
| if (overlayRef.current && isRevealing) { | ||
| overlayRef.current.scrollTop = textarea.scrollTop | ||
| } | ||
| } | ||
|
|
||
| textarea.addEventListener('scroll', syncScroll) | ||
|
|
||
| window.addEventListener('resize', updateDimensions) | ||
|
|
||
| syncScroll() | ||
|
|
||
| return () => { | ||
| textarea.removeEventListener('scroll', syncScroll) | ||
| window.removeEventListener('resize', updateDimensions) | ||
| if (observer) { | ||
| observer.disconnect() | ||
| } | ||
| } | ||
| }, [textareaRef, isRevealing, paragraphs]) | ||
|
|
||
| if (!isRevealing || paragraphs.length === 0) { | ||
| return null | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| ref={overlayRef} | ||
| className={`ds-prompt-input__text-reveal-overlay ${className || ''}`} | ||
| aria-hidden="true" | ||
| > | ||
| {paragraphs.map((paragraph, index) => ( | ||
| <p | ||
| key={index} | ||
| className={`ds-prompt-input__text-reveal-paragraph ${ | ||
|
||
| index === lastParagraphIndex ? 'ds-prompt-input__text-reveal-paragraph--animating' : '' | ||
| }`} | ||
| > | ||
| {paragraph} | ||
| </p> | ||
| ))} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| TextRevealOverlay.propTypes = { | ||
| text: PropTypes.string, | ||
| isRevealing: PropTypes.bool, | ||
| lastParagraphIndex: PropTypes.number, | ||
| className: PropTypes.string, | ||
| textareaRef: PropTypes.object, | ||
| onAnimationComplete: PropTypes.func | ||
| } | ||
|
|
||
| TextRevealOverlay.defaultProps = { | ||
| text: '', | ||
| isRevealing: false, | ||
| lastParagraphIndex: -1, | ||
| className: '', | ||
| textareaRef: null, | ||
| onAnimationComplete: null | ||
| } | ||
|
|
||
| export default TextRevealOverlay | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
|
|
||
| import { useState, useEffect, useRef, useCallback } from 'react' | ||
|
|
||
| function useProgressiveTextReveal({ | ||
| onComplete = () => {}, | ||
| paragraphDelay = 400, | ||
| onCompleteTimeout = 600, | ||
| onParagraphAdded = () => {} | ||
| } = {}) { | ||
| const [isRevealing, setIsRevealing] = useState(false) | ||
| const [currentText, setCurrentText] = useState('') | ||
| const [newParagraphAdded, setNewParagraphAdded] = useState(false) | ||
| const [lastParagraphIndex, setLastParagraphIndex] = useState(-1) | ||
| const timeoutsRef = useRef([]) | ||
| const paragraphsRef = useRef([]) | ||
| const currentIndexRef = useRef(0) | ||
|
|
||
| const clearAllTimeouts = useCallback(() => { | ||
| timeoutsRef.current.forEach(timeoutId => clearTimeout(timeoutId)) | ||
| timeoutsRef.current = [] | ||
| }, []) | ||
|
|
||
| // Reset the reveal state | ||
| const resetTextReveal = useCallback(() => { | ||
| clearAllTimeouts() | ||
| setCurrentText('') | ||
| setIsRevealing(false) | ||
| setNewParagraphAdded(false) | ||
| setLastParagraphIndex(-1) | ||
| paragraphsRef.current = [] | ||
| currentIndexRef.current = 0 | ||
| }, [clearAllTimeouts]) | ||
|
|
||
| // This function adds a single paragraph with animation trigger | ||
| const addNextParagraph = useCallback(() => { | ||
| if (currentIndexRef.current >= paragraphsRef.current.length) { | ||
|
|
||
| // Allow time for the final animation to complete before signaling done | ||
| const finalTimeout = setTimeout(() => { | ||
| onComplete(currentText) | ||
| }, onCompleteTimeout) | ||
|
|
||
| timeoutsRef.current.push(finalTimeout) | ||
| return | ||
| } | ||
|
|
||
| // First paragraph or append with double newline | ||
| const paragraphToAdd = paragraphsRef.current[currentIndexRef.current] | ||
|
|
||
| if (currentIndexRef.current === 0) { | ||
| setCurrentText(paragraphToAdd) | ||
| } else { | ||
| setCurrentText(prev => `${prev}\n\n${paragraphToAdd}`) | ||
| } | ||
|
|
||
| // Track the index of the paragraph we just added | ||
| setLastParagraphIndex(currentIndexRef.current) | ||
|
|
||
| // Signal that a new paragraph was added (for animation) | ||
| setNewParagraphAdded(true) | ||
|
|
||
| // Call the paragraph added callback | ||
| if (onParagraphAdded) { | ||
| onParagraphAdded(currentIndexRef.current) | ||
| } | ||
|
|
||
| // Reset animation flag after animation completes | ||
| const resetTimeout = setTimeout(() => { | ||
| setNewParagraphAdded(false) | ||
| }, 500) | ||
|
|
||
| timeoutsRef.current.push(resetTimeout) | ||
|
|
||
|
|
||
| currentIndexRef.current++ | ||
|
|
||
| // Schedule next paragraph if there are more | ||
| if (currentIndexRef.current < paragraphsRef.current.length) { | ||
| const nextTimeout = setTimeout(addNextParagraph, paragraphDelay) | ||
| timeoutsRef.current.push(nextTimeout) | ||
| } | ||
| }, [currentText, onComplete, onParagraphAdded, paragraphDelay]) | ||
|
|
||
| // Start revealing text paragraph by paragraph | ||
| const revealText = useCallback((text) => { | ||
| if (!text) return | ||
|
|
||
| resetTextReveal() | ||
| setIsRevealing(true) | ||
|
|
||
| // Split text into paragraphs (handles different line break formats) | ||
| const paragraphs = text.split(/\n{2,}|\r\n{2,}/) | ||
| .map(p => p.trim()) | ||
| .filter(p => p.length > 0) | ||
|
|
||
| if (paragraphs.length === 0) { | ||
| setIsRevealing(false) | ||
| return | ||
| } | ||
|
|
||
| paragraphsRef.current = paragraphs | ||
| currentIndexRef.current = 0 | ||
|
|
||
|
|
||
| addNextParagraph() | ||
|
|
||
| }, [resetTextReveal, addNextParagraph]) | ||
|
|
||
|
|
||
| useEffect(() => { | ||
| return () => clearAllTimeouts() | ||
| }, [clearAllTimeouts]) | ||
|
|
||
| return { | ||
| revealText, | ||
| resetTextReveal, | ||
| currentText, | ||
| isRevealing, | ||
| newParagraphAdded, | ||
| lastParagraphIndex | ||
| } | ||
| } | ||
|
|
||
| export default useProgressiveTextReveal |
Uh oh!
There was an error while loading. Please reload this page.