123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- import {useLayoutEffect, useRef} from 'react';
- import styled from '@emotion/styled';
- import * as Sentry from '@sentry/react';
- interface Props {
- children: React.ReactNode;
- }
- export function AutoSizedText({children}: Props) {
- const childRef = useRef<HTMLDivElement>(null);
- const fontSize = useRef<number>(0);
- const fontSizeLowerBound = useRef<number>(0);
- const fontSizeUpperBound = useRef<number>(0);
- useLayoutEffect(() => {
- const childElement = childRef.current; // This is `SizedChild`
- const parentElement = childRef.current?.parentElement; // This is the parent of `AutoSizedText`
- if (!childElement || !parentElement) {
- return undefined;
- }
- if (!window.ResizeObserver) {
- // `ResizeObserver` is missing in a test environment. In this case,
- // run one iteration of the resize behaviour so a test can at least
- // verify that the component doesn't crash.
- const childDimensions = getElementDimensions(childElement);
- const parentDimensions = getElementDimensions(parentElement);
- adjustFontSize(childDimensions, parentDimensions);
- return undefined;
- }
- // On component first mount, register a `ResizeObserver` on the containing element. The handler fires
- // on component mount, and every time the element changes size after that
- const observer = new ResizeObserver(entries => {
- // The entries list contains an array of every observed item. Here it is only one element
- const entry = entries[0];
- if (!entry) {
- return;
- }
- // The resize handler passes the parent's dimensions, so we don't have to get the bounding box
- const parentDimensions = entry.contentRect;
- // Reset the iteration parameters
- fontSizeLowerBound.current = 0;
- fontSizeUpperBound.current = parentDimensions.height;
- let iterationCount = 0;
- const span = Sentry.startInactiveSpan({
- op: 'function',
- name: 'AutoSizedText.iterate',
- onlyIfParent: true,
- });
- // Run the resize iteration in a loop. This blocks the main UI thread and prevents
- // visible layout jitter. If this was done through a `ResizeObserver` or React State
- // each step in the resize iteration would be visible to the user
- while (iterationCount <= ITERATION_LIMIT) {
- const childDimensions = getElementDimensions(childElement);
- const widthDifference = parentDimensions.width - childDimensions.width;
- const heightDifference = parentDimensions.height - childDimensions.height;
- const childFitsIntoParent = heightDifference >= 0 && widthDifference >= 0;
- const childIsWithinWidthTolerance =
- Math.abs(widthDifference) <= MAXIMUM_DIFFERENCE;
- const childIsWithinHeightTolerance =
- Math.abs(heightDifference) <= MAXIMUM_DIFFERENCE;
- if (
- childFitsIntoParent &&
- (childIsWithinWidthTolerance || childIsWithinHeightTolerance)
- ) {
- // Stop the iteration, we've found a fit!
- span.setAttribute('widthDifference', widthDifference);
- span.setAttribute('heightDifference', heightDifference);
- break;
- }
- adjustFontSize(childDimensions, parentDimensions);
- iterationCount += 1;
- }
- span.setAttribute('iterationCount', iterationCount);
- span.end();
- });
- observer.observe(parentElement);
- return () => {
- observer.disconnect();
- };
- }, []);
- const adjustFontSize = (childDimensions: Dimensions, parentDimensions: Dimensions) => {
- const childElement = childRef.current;
- if (!childElement) {
- return;
- }
- let newFontSize;
- if (
- childDimensions.width > parentDimensions.width ||
- childDimensions.height > parentDimensions.height
- ) {
- // The element is bigger than the parent, scale down
- newFontSize = (fontSizeLowerBound.current + fontSize.current) / 2;
- fontSizeUpperBound.current = fontSize.current;
- } else if (
- childDimensions.width < parentDimensions.width ||
- childDimensions.height < parentDimensions.height
- ) {
- // The element is smaller than the parent, scale up
- newFontSize = (fontSizeUpperBound.current + fontSize.current) / 2;
- fontSizeLowerBound.current = fontSize.current;
- }
- // Store font size in a ref so we don't have to measure styles to get it
- fontSize.current = newFontSize;
- childElement.style.fontSize = `${newFontSize}px`;
- };
- return <SizedChild ref={childRef}>{children}</SizedChild>;
- }
- const SizedChild = styled('div')`
- display: inline-block;
- `;
- const ITERATION_LIMIT = 20;
- // The maximum difference strongly affects the number of iterations required.
- // A value of 10 means that matches are often found in fewer than 5 iterations.
- // A value of 5 raises it to 6-7. A value of 1 brings it closer to 10. A value of
- // 0 never converges.
- // Note that on modern computers, even with 6x CPU throttling the iterations usually
- // finish in under 5ms.
- const MAXIMUM_DIFFERENCE = 1; // px
- type Dimensions = {
- height: number;
- width: number;
- };
- function getElementDimensions(element: HTMLElement): Dimensions {
- const bbox = element.getBoundingClientRect();
- return {
- width: bbox.width,
- height: bbox.height,
- };
- }
|