-
Notifications
You must be signed in to change notification settings - Fork 110
feat: add useTypingEffect hook for typing animations #137
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
Open
sukeshhublikar
wants to merge
2
commits into
DavidHDev:main
Choose a base branch
from
sukeshhublikar:feat/114-useTypingEffect-Hook
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+665
−0
Open
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| import React, { useState } from 'react'; | ||
| import { useTypingEffect } from 'react-haiku'; | ||
|
|
||
| export const UseTypingEffectDemo = () => { | ||
| const [customText, setCustomText] = useState('Hello, welcome to the typing effect demo!'); | ||
| const [isCompleted, setIsCompleted] = useState(false); | ||
|
|
||
| const { | ||
| displayedText, | ||
| isTyping, | ||
| startTyping, | ||
| reset | ||
| } = useTypingEffect(customText, { | ||
| baseDelay: 50, | ||
| randomDelay: 100, | ||
| onComplete: () => setIsCompleted(true) | ||
| }); | ||
|
|
||
| const handleStartTyping = () => { | ||
| setIsCompleted(false); | ||
| startTyping(); | ||
| }; | ||
|
|
||
| const handleReset = () => { | ||
| setIsCompleted(false); | ||
| reset(); | ||
| }; | ||
|
|
||
| const handleTextChange = (e) => { | ||
| setCustomText(e.target.value || 'Hello, welcome to the typing effect demo!'); | ||
| setIsCompleted(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <div style={{ | ||
| padding: '20px', | ||
| border: '1px solid #ddd', | ||
| borderRadius: '8px', | ||
| fontFamily: 'monospace' | ||
| }}> | ||
| <h3>useTypingEffect Demo</h3> | ||
|
|
||
| <div style={{ marginBottom: '20px' }}> | ||
| <label htmlFor="text-input" style={{ display: 'block', marginBottom: '8px' }}> | ||
| Text to animate: | ||
| </label> | ||
| <input | ||
| id="text-input" | ||
| type="text" | ||
| value={customText} | ||
| onChange={handleTextChange} | ||
| style={{ | ||
| width: '100%', | ||
| padding: '8px', | ||
| borderRadius: '4px', | ||
| border: '1px solid #ccc', | ||
| fontFamily: 'monospace' | ||
| }} | ||
| placeholder="Enter text to animate..." | ||
| /> | ||
| </div> | ||
|
|
||
| <div style={{ | ||
| minHeight: '60px', | ||
| padding: '15px', | ||
| backgroundColor: '#f5f5f5', | ||
| borderRadius: '4px', | ||
| marginBottom: '20px', | ||
| fontSize: '18px', | ||
| lineHeight: '1.4' | ||
| }}> | ||
| <span>{displayedText}</span> | ||
| {isTyping && ( | ||
| <span style={{ | ||
| animation: 'blink 1s infinite', | ||
| marginLeft: '2px' | ||
| }}>|</span> | ||
| )} | ||
| </div> | ||
|
|
||
| <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> | ||
| <button | ||
| onClick={handleStartTyping} | ||
| disabled={isTyping} | ||
| style={{ | ||
| padding: '10px 20px', | ||
| backgroundColor: isTyping ? '#ccc' : '#007bff', | ||
| color: 'white', | ||
| border: 'none', | ||
| borderRadius: '4px', | ||
| cursor: isTyping ? 'not-allowed' : 'pointer', | ||
| fontSize: '14px' | ||
| }} | ||
| > | ||
| {isTyping ? 'Typing...' : 'Start Typing'} | ||
| </button> | ||
|
|
||
| <button | ||
| onClick={handleReset} | ||
| style={{ | ||
| padding: '10px 20px', | ||
| backgroundColor: '#6c757d', | ||
| color: 'white', | ||
| border: 'none', | ||
| borderRadius: '4px', | ||
| cursor: 'pointer', | ||
| fontSize: '14px' | ||
| }} | ||
| > | ||
| Reset | ||
| </button> | ||
| </div> | ||
|
|
||
| <div style={{ fontSize: '14px', color: '#666' }}> | ||
| <p> | ||
| <strong>Status:</strong> { | ||
| isTyping ? '🔄 Typing in progress...' : | ||
| isCompleted ? '✅ Typing completed!' : | ||
| '⏸️ Ready to start' | ||
| } | ||
| </p> | ||
| <p> | ||
| <strong>Characters:</strong> {displayedText.length} / {customText.length} | ||
| </p> | ||
| </div> | ||
|
|
||
| <style jsx>{` | ||
| @keyframes blink { | ||
| 0%, 50% { opacity: 1; } | ||
| 51%, 100% { opacity: 0; } | ||
| } | ||
| `}</style> | ||
| </div> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # useTypingEffect() | ||
|
|
||
| The `useTypingEffect()` hook simulates a typing effect by progressively updating the state with randomized delays for a natural feel. | ||
|
|
||
| ### Import | ||
|
|
||
| ```jsx | ||
| import { useTypingEffect } from 'react-haiku'; | ||
| ``` | ||
|
|
||
| ### Usage | ||
|
|
||
| import { UseTypingEffectDemo } from '../../demo/UseTypingEffectDemo.jsx'; | ||
|
|
||
| <UseTypingEffectDemo /> | ||
|
|
||
| ```jsx | ||
| import { useTypingEffect } from 'react-haiku'; | ||
|
|
||
| export const Component = () => { | ||
| const { displayedText, isTyping, startTyping, reset } = useTypingEffect( | ||
| 'Hello, welcome to my website!', | ||
| { | ||
| baseDelay: 50, | ||
| randomDelay: 100, | ||
| onComplete: () => console.log('Typing completed!') | ||
| } | ||
| ); | ||
|
|
||
| return ( | ||
| <div> | ||
| <p style={{ fontFamily: 'monospace', fontSize: '18px' }}> | ||
| {displayedText} | ||
| {isTyping && <span>|</span>} | ||
| </p> | ||
| <button onClick={startTyping} disabled={isTyping}> | ||
| {isTyping ? 'Typing...' : 'Start Typing'} | ||
| </button> | ||
| <button onClick={reset}>Reset</button> | ||
| </div> | ||
| ); | ||
| }; | ||
| ``` | ||
|
|
||
| ### Parameters | ||
|
|
||
| | Parameter | Type | Required | Default | Description | | ||
| |-----------|------|----------|---------|-------------| | ||
| | `text` | `string` | ✅ | - | The text to animate. Must be a non-empty string. | | ||
| | `options` | `UseTypingEffectOptions` | ❌ | `{}` | Configuration options for the typing effect. | | ||
|
|
||
| ### Options | ||
|
|
||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `baseDelay` | `number` | `50` | Base delay between characters in milliseconds. | | ||
| | `randomDelay` | `number` | `100` | Maximum additional random delay in milliseconds for natural variation. | | ||
| | `onComplete` | `() => void` | `undefined` | Callback function called when typing animation completes. | | ||
|
|
||
| ### Return Value | ||
|
|
||
| The hook returns an object with the following properties: | ||
|
|
||
| | Property | Type | Description | | ||
| |----------|------|-------------| | ||
| | `displayedText` | `string` | The currently displayed portion of the text. | | ||
| | `isTyping` | `boolean` | Whether the typing animation is currently running. | | ||
| | `startTyping` | `() => void` | Function to start the typing animation. Resets state if called multiple times. | | ||
| | `reset` | `() => void` | Function to reset the displayed text to empty and stop any active animation. | | ||
|
|
||
| ### Examples | ||
|
|
||
| #### Basic Usage | ||
|
|
||
| ```jsx | ||
| const { displayedText, startTyping } = useTypingEffect('Hello World!'); | ||
| ``` | ||
|
|
||
| #### With Custom Timing | ||
|
|
||
| ```jsx | ||
| const { displayedText, isTyping, startTyping } = useTypingEffect( | ||
| 'This types slower with more variation...', | ||
| { | ||
| baseDelay: 100, // Slower base speed | ||
| randomDelay: 200 // More random variation | ||
| } | ||
| ); | ||
| ``` | ||
|
|
||
| #### With Completion Callback | ||
|
|
||
| ```jsx | ||
| const [isComplete, setIsComplete] = useState(false); | ||
|
|
||
| const { displayedText, startTyping } = useTypingEffect( | ||
| 'Thanks for reading!', | ||
| { | ||
| onComplete: () => setIsComplete(true) | ||
| } | ||
| ); | ||
| ``` | ||
|
|
||
| #### Multiple Typing Effects | ||
|
|
||
| ```jsx | ||
| const intro = useTypingEffect('Hello, I am'); | ||
| const name = useTypingEffect('John Doe', { baseDelay: 80 }); | ||
|
|
||
| const handleStart = () => { | ||
| intro.startTyping(); | ||
| // Start second effect after first completes | ||
| setTimeout(() => name.startTyping(), 2000); | ||
| }; | ||
| ``` | ||
|
|
||
| ### Features | ||
|
|
||
| - ✅ **Natural Typing Feel**: Uses randomized delays between characters | ||
| - ✅ **Configurable Speed**: Customize base delay and random variation | ||
| - ✅ **Reset Support**: Multiple calls to `startTyping()` reset and restart | ||
| - ✅ **Completion Callback**: Know when typing animation finishes | ||
| - ✅ **TypeScript Support**: Fully typed with comprehensive interfaces | ||
| - ✅ **Cleanup**: Automatic timer cleanup on unmount | ||
|
|
||
| ### Notes | ||
|
|
||
| - The `text` parameter must be a non-empty string, otherwise the hook will throw an error | ||
| - Calling `startTyping()` multiple times will reset the current animation and start fresh | ||
| - The hook automatically cleans up any pending timers when the component unmounts | ||
| - Random delays create a more natural typing rhythm compared to fixed intervals | ||
| - The `isTyping` state can be used to show/hide typing indicators or disable controls | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { useState, useCallback, useRef, useEffect } from 'react'; | ||
|
|
||
| interface UseTypingEffectOptions { | ||
| /** Base delay between characters in milliseconds */ | ||
| baseDelay?: number; | ||
| /** Maximum additional random delay in milliseconds */ | ||
| randomDelay?: number; | ||
| /** Function called when typing animation completes */ | ||
| onComplete?: () => void; | ||
| } | ||
|
|
||
| interface UseTypingEffectReturn { | ||
| /** Current displayed text */ | ||
| displayedText: string; | ||
| /** Whether the typing animation is currently running */ | ||
| isTyping: boolean; | ||
| /** Function to start the typing animation */ | ||
| startTyping: () => void; | ||
| /** Function to reset the displayed text */ | ||
| reset: () => void; | ||
| } | ||
|
|
||
| /** | ||
| * Hook that simulates a typing effect by progressively updating the state | ||
| * with randomized delays for a natural feel. | ||
| * | ||
| * @param text - The text to animate (must be non-empty string) | ||
| * @param options - Configuration options for the typing effect | ||
| * @returns Object containing displayed text, typing state, and control functions | ||
| */ | ||
| export function useTypingEffect( | ||
| text: string, | ||
| options: UseTypingEffectOptions = {} | ||
| ): UseTypingEffectReturn { | ||
| const { | ||
| baseDelay = 50, | ||
| randomDelay = 100, | ||
| onComplete | ||
| } = options; | ||
|
|
||
| // Validate text parameter | ||
| if (typeof text !== 'string' || text.length === 0) { | ||
| throw new Error('useTypingEffect: text parameter must be a non-empty string'); | ||
| } | ||
|
|
||
| const [displayedText, setDisplayedText] = useState(''); | ||
| const [isTyping, setIsTyping] = useState(false); | ||
| const timeoutRef = useRef<number | null>(null); | ||
| const currentIndexRef = useRef(0); | ||
|
|
||
| // Clear any existing timeout when component unmounts | ||
| useEffect(() => { | ||
| return () => { | ||
| if (timeoutRef.current) { | ||
| window.clearTimeout(timeoutRef.current); | ||
| } | ||
| }; | ||
| }, []); | ||
|
|
||
| const reset = useCallback(() => { | ||
| if (timeoutRef.current) { | ||
| window.clearTimeout(timeoutRef.current); | ||
| timeoutRef.current = null; | ||
| } | ||
| setDisplayedText(''); | ||
| setIsTyping(false); | ||
| currentIndexRef.current = 0; | ||
| }, []); | ||
|
|
||
| const typeNextCharacter = useCallback(() => { | ||
| if (currentIndexRef.current < text.length) { | ||
| setDisplayedText(text.slice(0, currentIndexRef.current + 1)); | ||
| currentIndexRef.current += 1; | ||
|
|
||
| // Calculate randomized delay for natural typing feel | ||
| const delay = baseDelay + Math.random() * randomDelay; | ||
|
|
||
| timeoutRef.current = window.setTimeout(typeNextCharacter, delay); | ||
| } else { | ||
| // Typing complete | ||
| setIsTyping(false); | ||
| onComplete?.(); | ||
| } | ||
| }, [text, baseDelay, randomDelay, onComplete]); | ||
|
|
||
| const startTyping = useCallback(() => { | ||
| // Reset state before starting (handles multiple calls) | ||
| reset(); | ||
|
|
||
| setIsTyping(true); | ||
| currentIndexRef.current = 0; | ||
|
|
||
| // Start typing with initial delay | ||
| const initialDelay = baseDelay + Math.random() * randomDelay; | ||
| timeoutRef.current = window.setTimeout(typeNextCharacter, initialDelay); | ||
| }, [reset, baseDelay, randomDelay, typeNextCharacter]); | ||
|
|
||
| return { | ||
| displayedText, | ||
| isTyping, | ||
| startTyping, | ||
| reset | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Parameters, options, and return value should all be at the bottom of the demo, and be simple bullet point lists instead of tables.