import { useLayoutEffect, useRef } from 'react'; import { cn } from '../lib/utils'; export type Dimensions = { height: number; width: number; }; export type AutoSizedTextProps = { children: React.ReactNode; className?: string; maxHeight?: number; useRem?: boolean; }; const ITERATION_LIMIT = 20; const MAXIMUM_DIFFERENCE = 1; // px function getElementDimensions(element: HTMLElement): Dimensions { const bbox = element.getBoundingClientRect(); return { width: bbox.width, height: bbox.height, }; } function getBaseFontSize(): number { try { const fontSize = getComputedStyle(document.documentElement).fontSize; const parsed = parseFloat(fontSize); // Check if we got a valid number if (!Number.isFinite(parsed)) { return 16; } return parsed; } catch (error) { // Fallback to browser default if anything goes wrong return 16; } } function pxToRem(px: number): number { return px / getBaseFontSize(); } export function AutoSizedText({ children, className, maxHeight, useRem = false, }: AutoSizedTextProps) { const childRef = useRef(null); const fontSize = useRef(0); const fontSizeLowerBound = useRef(0); const fontSizeUpperBound = useRef(0); const adjustFontSize = (childDimensions: Dimensions, parentDimensions: Dimensions) => { const childElement = childRef.current; if (!childElement) { return; } let newFontSize: number; const targetHeight = maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height; const isElementTooBig = childDimensions.width > parentDimensions.width || childDimensions.height > targetHeight; if (isElementTooBig) { // Scale down if element is bigger than target newFontSize = (fontSizeLowerBound.current + fontSize.current) / 2; fontSizeUpperBound.current = fontSize.current; } else if ( childDimensions.width < parentDimensions.width || childDimensions.height < parentDimensions.height ) { // Scale up if element is smaller than target newFontSize = (fontSizeUpperBound.current + fontSize.current) / 2; fontSizeLowerBound.current = fontSize.current; } fontSize.current = newFontSize; // Convert to rem if useRem is true const displayFontSize = useRem ? `${pxToRem(newFontSize)}rem` : `${newFontSize}px`; childElement.style.fontSize = displayFontSize; }; useLayoutEffect(() => { const childElement = childRef.current; const parentElement = childRef.current?.parentElement; if (!childElement || !parentElement) { return; } const observer = new ResizeObserver((entries) => { const entry = entries[0]; if (!entry) { return; } const parentDimensions = entry.contentRect; // Reset iteration parameters fontSizeLowerBound.current = 0; fontSizeUpperBound.current = parentDimensions.height; let iterationCount = 0; while (iterationCount <= ITERATION_LIMIT) { const childDimensions = getElementDimensions(childElement); const targetHeight = maxHeight && maxHeight < parentDimensions.height ? maxHeight : parentDimensions.height; const widthDifference = parentDimensions.width - childDimensions.width; const heightDifference = targetHeight - childDimensions.height; const childFitsIntoParent = heightDifference >= 0 && widthDifference >= 0; const isWithinTolerance = Math.abs(widthDifference) <= MAXIMUM_DIFFERENCE || Math.abs(heightDifference) <= MAXIMUM_DIFFERENCE; if (childFitsIntoParent && isWithinTolerance) { break; } adjustFontSize(childDimensions, parentDimensions); iterationCount += 1; } }); observer.observe(parentElement); return () => { observer.disconnect(); }; }, [maxHeight, useRem]); return (
{children}
); }