Skip to content

Instantly share code, notes, and snippets.

@thefat32
Last active July 4, 2023 19:44
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 thefat32/478ff0ebd438123b2fd5028755ab427d to your computer and use it in GitHub Desktop.
Save thefat32/478ff0ebd438123b2fd5028755ab427d to your computer and use it in GitHub Desktop.
React fit text component using measureText(), binary searchand ResizeObservable
import React, { useLayoutEffect, useState } from 'react';
import useResizeObserver from '../../hooks/useResizeObserver';
class NotSupportedError extends Error {
constructor(message: string) {
super(message);
this.name = 'NotSupportedError';
}
}
const dummyCanvas = document.createElement('canvas');
const ctx = dummyCanvas.getContext('2d');
if (!ctx) throw new NotSupportedError('Canvas 2d context not supported');
const doesOverflow = (
font: string,
text: string,
maxWidth: number,
maxHeight: number,
widthOnly: boolean,
lineHeight: number,
fontSize: number
) => {
ctx.font = font;
const metrics = ctx.measureText(text);
return metrics.width > maxWidth || (!widthOnly && fontSize * lineHeight > maxHeight);
};
interface Props extends Exclude<React.HTMLAttributes<HTMLDivElement>, 'style'> {
children?: string;
minSize?: number;
maxSize?: number;
lineHeight?: number;
widthOnly?: boolean;
rootStyle?: React.CSSProperties;
verticalAlign?: 'center' | 'start' | 'end';
justifyContent?: 'center' | 'start' | 'end';
maxHeight?: number;
}
const THRESHOLD = 10;
const TextFit = ({
children,
minSize = 12,
maxSize = 512,
lineHeight = 1.2,
widthOnly = false,
rootStyle,
verticalAlign,
justifyContent,
maxHeight: maxHeightProp,
...rest
}: Props) => {
if (typeof children !== 'string')
throw new TypeError(`Child of TextFit must be a string, received ${typeof children}`);
const textContainerRef = React.useRef<HTMLDivElement | null>(null);
const [fontSize, setFontSize] = useState(minSize);
const { width: containerWidth, height: containerHeight } = useResizeObserver({
ref: textContainerRef
});
useLayoutEffect(() => {
if (textContainerRef.current) {
const {
width: computedWidth,
height: computedHeight,
fontStyle: computedFontStyle,
fontVariant: computedFontVariant,
fontWeight: computedFontWeight,
fontFamily: computedFontFamily
} = getComputedStyle(textContainerRef.current);
const maxWidth = parseInt(computedWidth) - THRESHOLD;
const maxHeight = maxHeightProp ?? parseInt(computedHeight) - THRESHOLD;
// optimization check min and max first
// min
const minOverflow = doesOverflow(
`${computedFontStyle} ${computedFontVariant} ${computedFontWeight} ${minSize}px / ${
lineHeight * minSize
}px ${computedFontFamily}`,
children,
maxWidth,
maxHeight,
widthOnly,
lineHeight,
minSize
);
if (minOverflow) {
setFontSize(minSize);
return;
}
// max
const maxOverflow = doesOverflow(
`${computedFontStyle} ${computedFontVariant} ${computedFontWeight} ${maxSize}px / ${
lineHeight * maxSize
}px ${computedFontFamily}`,
children,
maxWidth,
maxHeight,
widthOnly,
lineHeight,
maxSize
);
if (!maxOverflow) {
setFontSize(maxSize);
return;
}
// binary search
let low = minSize;
let high = maxSize;
let result = true;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const currentResult = doesOverflow(
`${computedFontStyle} ${computedFontVariant} ${computedFontWeight} ${mid}px / ${
lineHeight * mid
}px ${computedFontFamily}`,
children,
maxWidth,
maxHeight,
widthOnly,
lineHeight,
mid
);
if (currentResult === true) {
// Move to the left half
high = mid - 1;
} else {
// Move to the right half
low = mid + 1;
}
}
setFontSize(result ? low - 1 : low);
}
}, [
maxSize,
minSize,
lineHeight,
children,
widthOnly,
maxHeightProp,
containerWidth,
containerHeight
]);
return (
<div style={rootStyle}>
<div
style={{
fontSize,
lineHeight,
padding: 0,
margin: 0,
width: '100%',
height: widthOnly ? undefined : '100%',
display: 'flex',
alignItems: verticalAlign,
justifyContent
}}
{...rest}
ref={textContainerRef}>
{children}
</div>
</div>
);
};
export default TextFit;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment