Created
September 10, 2024 18:09
-
-
Save EvanMarie/648baca566cb16ab28f9989ab08e421b to your computer and use it in GitHub Desktop.
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
| interface TextAreaProps | |
| extends Omit< | |
| React.TextareaHTMLAttributes<HTMLTextAreaElement>, | |
| "value" | "onChange" | |
| > { | |
| className?: string; | |
| value?: string; | |
| onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void; | |
| style?: React.CSSProperties; | |
| textAreaHeight?: string; | |
| textAreaWidth?: string; | |
| autoFocus?: boolean; | |
| isEditable?: boolean; | |
| } | |
| export function TextAreaNumberedLines({ | |
| className = "", | |
| value: propValue, | |
| onChange, | |
| textAreaHeight = "h-full max-h-[56vh]", | |
| style = { | |
| resize: "none", | |
| fontSize: "15px", | |
| lineHeight: "23px", | |
| minHeight: textAreaHeight.replace("h-", ""), | |
| }, | |
| textAreaWidth = "w-full", | |
| autoFocus = false, | |
| ...props | |
| }: TextAreaProps) { | |
| const [value, setValue] = useState(propValue || ""); | |
| const [lineCount, setLineCount] = useState<number>(1); | |
| const textAreaRef = useRef<HTMLTextAreaElement | null>(null); | |
| const lineNumberRef = useRef<HTMLDivElement | null>(null); | |
| // Calculate the number of lines in the textarea, including wrapped lines | |
| const updateLineCount = () => { | |
| if (textAreaRef.current) { | |
| const textareaLineHeight = 23; // Match this with your CSS line height | |
| const textareaScrollHeight = textAreaRef.current.scrollHeight; | |
| // If the textarea is empty, set line count to 1 (empty line) | |
| if (textAreaRef.current.value.trim() === "") { | |
| setLineCount(1); | |
| } else { | |
| const totalLines = Math.ceil(textareaScrollHeight / textareaLineHeight); | |
| setLineCount(totalLines); // Update line count based on content height | |
| } | |
| } | |
| }; | |
| const updateHeight = () => { | |
| if (textAreaRef.current) { | |
| textAreaRef.current.style.height = "auto"; | |
| textAreaRef.current.style.height = `${textAreaRef.current.scrollHeight}px`; // Dynamically adjust height based on content | |
| } | |
| }; | |
| // Recalculate lines and height after the component is mounted | |
| useLayoutEffect(() => { | |
| if (propValue !== undefined) { | |
| setValue(propValue); | |
| // Use setTimeout to delay recalculation to ensure content is fully loaded | |
| setTimeout(() => { | |
| updateLineCount(); | |
| updateHeight(); | |
| }, 0); // Small delay to allow layout to settle | |
| } | |
| }, [propValue]); | |
| // Initial layout adjustment | |
| useLayoutEffect(() => { | |
| // Delay recalculation after the initial render | |
| setTimeout(() => { | |
| updateLineCount(); | |
| updateHeight(); | |
| }, 0); | |
| }, []); | |
| // Sync scrolling between textarea and line number | |
| const handleScroll = () => { | |
| if (textAreaRef.current && lineNumberRef.current) { | |
| lineNumberRef.current.scrollTop = textAreaRef.current.scrollTop; | |
| } | |
| }; | |
| // Handle text input and trigger updates | |
| const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => { | |
| const newValue = e.target.value; | |
| setValue(newValue); | |
| updateLineCount(); // Update line count on input | |
| updateHeight(); // Adjust height on input | |
| if (onChange) { | |
| onChange(e); // Call the external onChange handler if provided | |
| } | |
| }; | |
| // Generate all line numbers based on the total line count | |
| const renderedLines = Array.from({ length: lineCount }, (_, i) => ( | |
| <FlexFull key={i} className="h-[23px] items-center"> | |
| <Text className="text-xs text-col-950 text-stroke-5-950 text-right"> | |
| {i + 1} | |
| </Text> | |
| </FlexFull> | |
| )); | |
| const scrollRef = useRef<HTMLDivElement>(null); | |
| return ( | |
| // Main container with scroll progress and line numbers | |
| <TransitionFull className="relative"> | |
| <ScrollProgressBar containerRef={scrollRef} height="h-[0.5vh]" /> | |
| <Flex | |
| className={`${textAreaHeight} ${textAreaWidth} overflow-y-auto overflow-x-hidden textareaStyles p-0.5vh focus-border-[0.2vh] outline-col-300 insetGlow6Xl border-500-md`} | |
| ref={scrollRef} | |
| > | |
| <HStack className={`h-fit w-full items-start relative `} tabIndex={1}> | |
| {/* Line Numbers */} | |
| <VStack | |
| ref={lineNumberRef} | |
| className="h-fit" | |
| style={{ width: "3vh" }} | |
| gap="gap-[0px]" | |
| > | |
| {renderedLines} | |
| </VStack> | |
| {/* Text Area */} | |
| <textarea | |
| ref={textAreaRef} | |
| value={value} | |
| onChange={handleInput} | |
| onScroll={handleScroll} | |
| autoFocus={autoFocus} | |
| className="w-full h-fit bg-transparent focus-border-0 outline-none flex flex-1 overflow-hidden pb-[4vh]" | |
| style={style} | |
| {...props} | |
| /> | |
| </HStack> | |
| </Flex> | |
| </TransitionFull> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment