Files
documenso/packages/ui/primitives/auto-sized-text.tsx
ephraimduncan 5a0f438ee6 Merge branch 'main' into feat/prefetch-intent-navigation-links
# Conflicts:
#	apps/remix/app/components/general/app-header.tsx
#	apps/remix/app/components/general/app-nav-desktop.tsx
#	apps/remix/app/components/general/app-nav-mobile.tsx
#	apps/remix/app/components/general/folder/folder-card.tsx
#	apps/remix/app/components/general/folder/folder-grid.tsx
#	apps/remix/app/components/general/menu-switcher.tsx
#	apps/remix/app/components/general/org-menu-switcher.tsx
#	apps/remix/app/components/general/settings-nav-mobile.tsx
#	apps/remix/app/components/tables/admin-claims-table.tsx
#	apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#	apps/remix/app/components/tables/admin-organisations-table.tsx
#	apps/remix/app/components/tables/documents-table-action-button.tsx
#	apps/remix/app/components/tables/organisation-email-domains-table.tsx
#	apps/remix/app/components/tables/templates-table-action-dropdown.tsx
#	apps/remix/app/components/tables/user-billing-organisations-table.tsx
#	apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#	apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx
#	apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#	apps/remix/app/routes/_authenticated+/dashboard.tsx
#	apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#	apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#	packages/email/template-components/template-document-reminder.tsx
#	packages/ui/primitives/tooltip.tsx
2026-05-08 11:17:51 +00:00

144 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 = fontSize.current;
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>
);
}