mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
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<HTMLDivElement>(null);
|
|
|
|
const fontSize = useRef<number>(0);
|
|
const fontSizeLowerBound = useRef<number>(0);
|
|
const fontSizeUpperBound = useRef<number>(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 (
|
|
<div ref={childRef} className={cn('inline-block leading-none', className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|