Skip to content

Instantly share code, notes, and snippets.

@iseabock
Last active September 7, 2023 02:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iseabock/984fcea112233f7284a0c032ee25ca8c to your computer and use it in GitHub Desktop.
Save iseabock/984fcea112233f7284a0c032ee25ca8c to your computer and use it in GitHub Desktop.
Typewriter Component
import React, { useState, useEffect, useContext } from 'react';
import PropTypes from 'prop-types';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { TypewriterContext } from '../util/Context';
const BlinkingCursor = ({ sentenceComplete }) => {
const [show, setShow] = useState(true);
const [isBlinkOn, setIsBlinkOn] = useState(false);
// When sentence is complete, blink cursor for 7 seconds
useEffect(() => {
if (sentenceComplete) {
setIsBlinkOn(true);
setTimeout(() => {
setIsBlinkOn(false);
setShow((prevShow) => !prevShow);
}, 7000);
}
}, [sentenceComplete]);
// Remove cursor when blinking is done
useEffect(() => {
if (!isBlinkOn) return;
const timer = setInterval(() => {
setShow((prevShow) => !prevShow);
}, 500);
return () => {
clearInterval(timer);
};
}, [isBlinkOn]);
return <Box>{show ? <span>_</span> : <span style={{ color: 'transparent' }}>_</span>}</Box>;
};
const Typewriter = ({ sentence, typingSpeed, containerSize }) => {
const [currentText, setCurrentText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
const [fontSize, setFontSize] = useState(20);
// If other components don't need to know when the sentence is complete,
// replace with useState
const { sentenceComplete, setSentenceComplete } = useContext(TypewriterContext);
// Randomize typing speed to mimic human typing... sort of. Bigger number = slower btw
let speed = Math.floor(Math.random() * typingSpeed) + 25;
// Determine font size based on container width
// If you dont need to worry about the fontSize for the container
// remove the following useEffect and the #sentenceTest element in the render()
useEffect(() => {
var container = document.getElementById('sentenceTest');
var text = document.getElementById('text');
var testFontSize = 20;
text.style.fontSize = testFontSize + 'px';
while (Math.ceil(text.clientWidth) > container.clientWidth) {
testFontSize--;
text.style.fontSize = testFontSize + 'px';
setFontSize(`${testFontSize--}px`);
}
}, [fontSize]);
// Type out sentence
useEffect(() => {
const timer = setInterval(() => {
if (currentIndex < sentence.length) {
setCurrentText((prevText) => prevText + sentence[currentIndex]);
setCurrentIndex((prevIndex) => prevIndex + 1);
} else {
clearInterval(timer);
}
}, speed);
return () => {
clearInterval(timer);
};
}, [sentence, currentIndex, typingSpeed]);
useEffect(() => {
currentText.length === sentence.length && setSentenceComplete(true);
}, [currentText, sentence]);
return (
<Box
sx={{ width: '100%' }}
flexDirection="column"
alignItems="center"
justifyContent="center">
<Typography
component="div"
variant="courier"
sx={{
width: 'fit-content',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
margin: '0 auto',
whiteSpace: 'nowrap',
fontSize: fontSize
}}>
{currentText}
<BlinkingCursor sentenceComplete={sentenceComplete} />
</Typography>
<Box id="sentenceTest" sx={{ width: `${containerSize}rem` }}>
<Typography
component="div"
variant="courier"
id="text"
sx={{
whiteSpace: 'nowrap',
width: 'auto',
position: 'absolute',
color: 'transparent',
fontSize: fontSize
}}>
{sentence + '_'}
</Typography>
</Box>
</Box>
);
};
Typewriter.propTypes = {
sentence: PropTypes.string,
typingSpeed: PropTypes.number,
containerSize: PropTypes.number
};
BlinkingCursor.propTypes = {
sentenceComplete: PropTypes.bool
};
export default Typewriter;
// Usage
<Typewriter sentence="Some string" typingSpeed={80} containerSize={24} />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment