react-fitter
Version:
<h1 align="center">🫸 𝚛𝚎𝚊𝚌𝚝-𝚏𝚒𝚝𝚝𝚎𝚛 🫷</h1>
119 lines (117 loc) • 3.07 kB
JavaScript
// lib/Fitter.tsx
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
// lib/hooks.ts
import {
useEffect,
useLayoutEffect,
useMemo,
useRef
} from "react";
var debounce = (callback, delay) => {
let timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
};
var useDebounce = (callback, delay) => {
const callbackRef = useRef(callback);
useLayoutEffect(() => {
callbackRef.current = callback;
});
return useMemo(
() => debounce((...args) => callbackRef.current(...args), delay),
[delay]
);
};
var useResizeObserver = (ref, callback, enabled) => {
const width = useRef(0);
useEffect(() => {
if (!ref.current) return;
if (!enabled) return;
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const newWidth = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
if (newWidth !== width.current) {
width.current = newWidth;
callback();
}
}
});
observer.observe(ref.current);
return () => {
observer.disconnect();
};
}, [enabled]);
};
// lib/Fitter.tsx
import { jsx } from "react/jsx-runtime";
var Fitter = ({
children,
minSize = 0.25,
maxLines = 1,
settlePrecision = 0.01,
updateOnSizeChange = true,
resizeDebounceMs = 50
}) => {
const wrapperRef = useRef2(null);
const textRef = useRef2(null);
const [size, setSize] = useState(1);
const [settled, setSettled] = useState(false);
const [targetMax, setTargetMax] = useState(1);
useEffect2(() => {
if (settled) return;
if (!textRef.current) throw new Error("Could not access wrapper ref");
const lines = textRef.current.getClientRects().length;
const nextShrinkStep = (size - minSize) / 2;
const nextGrowStep = (targetMax - size) / 2;
const nextShrinkSize = size - nextShrinkStep;
const nextGrowSize = size + nextGrowStep;
const wantsToShrink = lines > maxLines;
if (wantsToShrink) {
if (nextShrinkStep < settlePrecision) {
setSettled(true);
return;
}
setTargetMax(size);
setSize(nextShrinkSize);
return;
}
if (nextGrowStep < settlePrecision) {
setSettled(true);
return;
}
setSize(nextGrowSize);
}, [size, maxLines, minSize, targetMax, settlePrecision, settled]);
const start = useDebounce(() => {
setTargetMax(1);
setSettled(false);
}, resizeDebounceMs);
useResizeObserver(wrapperRef, () => start(), updateOnSizeChange);
return /* @__PURE__ */ jsx(
"div",
{
ref: wrapperRef,
className: "react-fitter__wrapper",
style: {
lineHeight: 0
},
children: /* @__PURE__ */ jsx(
"span",
{
ref: textRef,
style: {
fontSize: `${size * 100}%`,
lineHeight: 1
},
className: "react-fitter__text",
children
}
)
}
);
};
export {
Fitter
};
//# sourceMappingURL=index.js.map