`) - the ref to be passed to the `ref` attribute of the `div`
46 | - `options` (optional) - an object with the following optional properties:
47 |
48 | - `logLevel` (`string`, default: `info`) - one of `debug`, `info`, `warn`, `error`, or `none`
49 | - `maxFontSize` (`number`, default: `100`) - maximum font size in percent
50 | - `minFontSize` (`number`, default: `20`) - minimum font size in percent
51 | - `onFinish` (`(fontSize: number) => void`, default: `undefined`) - function that is called when resizing
52 | finishes. The final fontSize is passed to the function as an argument.
53 | - `onStart` (`() => void`, default: `undefined`) - function that is called when resizing starts
54 | - `resolution` (`number`, default: `5`) - font size resolution to adjust to in percent
55 |
56 | ## Questions
57 |
58 | - Why doesn't it work with Flexbox `justify-content: flex-end;`?
59 | This appears [to be](https://stackoverflow.com/questions/36130760/use-justify-content-flex-end-and-to-have-vertical-scrollbar) [a bug](https://github.com/philipwalton/flexbugs/issues/53) with Flexbox. Try using CSS Grid or `margin-top: auto;`
60 | - What does the "reached `minFontSize = 20` without fitting text" message in the console mean?
61 | This means `use-fit-text` was not able to fit the text using the `minFontSize` setting of 20. To ensure the text fits, set `minFontSize` to a smaller value.
62 |
63 | ## Changelog
64 |
65 | - v2.4.0
66 | - handle case where `minFontSize` is set larger than the `fontSize` value needed to fit the text in the div. Log a message to the console in this case.
67 | - fix final adjustment calcuation
68 | - add `logLevel` option to control what is logged to the console
69 | - v2.3.0
70 | - automatically recalculate font size when content changes
71 | - fix bug where a recalculation was not done on resize if the text initially fit in the div
72 | - v2.2.0 - add `onStart` and `onFinish` callbacks
73 | - v2.1.3 - export `TOptions` TypeScript type
74 | - v2.1.2 - remove `/// ` in `dist/index.d.ts`
75 | - v2.1.0
76 | - fix SSR/prerender issue where text did not resize
77 | - suppress `useLayoutEffect` warning for server render
78 | - v2.0.0
79 | - use `ResizeObserver` to always recalculate on container resize
80 | - remove `recalcOnResize` option
81 | - `useLayoutEffect` instead of `useEffect` to avoid flashing
82 | - v1.2.1 - fix scrollbar issue in example
83 | - v1.2.0 - add `recalcOnResize` and other options
84 | - v1.1.0 - fix binary search bug
85 | - v1.0.2 - add example
86 | - v1.0.0 - initial release
87 |
--------------------------------------------------------------------------------
/examples/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import React, { useCallback, useState } from "react";
3 | import useFitText from "use-fit-text";
4 |
5 | /**
6 | * Example1 - basic example
7 | */
8 | function Example1() {
9 | const { fontSize, ref } = useFitText();
10 | return (
11 | <>
12 | Example 1 - basic example
13 |
17 | Lorem ipsum dolor sit amet, consectetur Lorem ipsum dolor sit amet,
18 | consectetur
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | /**
26 | * Example2 - non-wrapping text
27 | */
28 | function Example2() {
29 | const { fontSize, ref } = useFitText();
30 | return (
31 | <>
32 | Example 2 - non-wrapping text
33 |
37 |
38 | Lorem ipsum dolor sit amet, consectetur
39 |
40 |
41 |
42 | >
43 | );
44 | }
45 |
46 | /**
47 | * Example3 - demonstrates:
48 | * - recalculating font size on window resize
49 | * - `onStart` and `onFinish` callbacks
50 | */
51 | function Example3() {
52 | const onStart = useCallback(() => {
53 | console.log("Example 3 resizing started");
54 | }, []);
55 | const onFinish = useCallback((fontSize: number) => {
56 | console.log("Example 3 resizing finished", fontSize);
57 | }, []);
58 | const { fontSize, ref } = useFitText({ maxFontSize: 500, onStart, onFinish });
59 |
60 | return (
61 | <>
62 | Example 3 - recalculating on container change
63 | Demonstrates:
64 |
65 | recalculating font size on window resize
66 |
67 | onStart and{" "}
68 | onFinish callbacks (see log message
69 | in the console)
70 |
71 |
72 |
80 | Lorem ipsum dolor sit amet, consectetur
81 |
82 |
83 | >
84 | );
85 | }
86 |
87 | /**
88 | * Example4 - recalculating on content change
89 | */
90 | function Example4() {
91 | const [text, setText] = useState("Lorem ipsum dolor sit amet, consectetur");
92 | const { fontSize, ref } = useFitText();
93 |
94 | return (
95 | <>
96 | Example 4 - recalculating on content change
97 |
98 | Change the input text and the font size will be updated to fit the text
99 | in the div
100 |
101 |
113 |
114 | >
115 | );
116 | }
117 |
118 | /**
119 | * Example5 - fails to fit text because `fontSizeMin` is too big
120 | */
121 | function Example5() {
122 | const { fontSize, ref } = useFitText({
123 | maxFontSize: 285.7142857142857,
124 | minFontSize: 125.7142857142857,
125 | // Note: with `v2.3.0` and earlier, adding this non-referentially equal
126 | // `onFinish` callback caused a "Maximum update depth exceeded" error.
127 | // See https://github.com/saltycrane/use-fit-text/issues/9
128 | onFinish: () => {},
129 | });
130 |
131 | return (
132 | <>
133 |
134 | Example 5 - fails to fit text because fontSizeMin is too
135 | big. Shows a message in the console.
136 |
137 |
141 | Lorem ipsum dolor sit amet, consectetur Lorem ipsum dolor sit amet,
142 | consectetur
143 |
144 |
145 | >
146 | );
147 | }
148 |
149 | /**
150 | * Page
151 | */
152 | function Page() {
153 | return (
154 | <>
155 |
156 |
162 |
163 |
164 |
165 |
use-fit-text
166 |
176 |
177 |
178 |
179 |
Examples
180 |
181 |
182 |
183 |
184 |
185 |
186 | >
187 | );
188 | }
189 |
190 | export default Page;
191 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | useCallback,
3 | useEffect,
4 | useLayoutEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import ResizeObserver from "resize-observer-polyfill";
9 |
10 | export type TLogLevel = "debug" | "info" | "warn" | "error" | "none";
11 |
12 | export type TOptions = {
13 | logLevel?: TLogLevel;
14 | maxFontSize?: number;
15 | minFontSize?: number;
16 | onFinish?: (fontSize: number) => void;
17 | onStart?: () => void;
18 | resolution?: number;
19 | };
20 |
21 | const LOG_LEVEL: Record = {
22 | debug: 10,
23 | info: 20,
24 | warn: 30,
25 | error: 40,
26 | none: 100,
27 | };
28 |
29 | // Suppress `useLayoutEffect` warning when rendering on the server
30 | // https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85
31 | const useIsoLayoutEffect =
32 | typeof window !== "undefined" &&
33 | window.document &&
34 | window.document.createElement
35 | ? useLayoutEffect
36 | : useEffect;
37 |
38 | const useFitText = ({
39 | logLevel: logLevelOption = "info",
40 | maxFontSize = 100,
41 | minFontSize = 20,
42 | onFinish,
43 | onStart,
44 | resolution = 5,
45 | }: TOptions = {}) => {
46 | const logLevel = LOG_LEVEL[logLevelOption];
47 |
48 | const initState = useCallback(() => {
49 | return {
50 | calcKey: 0,
51 | fontSize: maxFontSize,
52 | fontSizePrev: minFontSize,
53 | fontSizeMax: maxFontSize,
54 | fontSizeMin: minFontSize,
55 | };
56 | }, [maxFontSize, minFontSize]);
57 |
58 | const ref = useRef(null);
59 | const innerHtmlPrevRef = useRef();
60 | const isCalculatingRef = useRef(false);
61 | const [state, setState] = useState(initState);
62 | const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;
63 |
64 | // Montior div size changes and recalculate on resize
65 | let animationFrameId: number | null = null;
66 | const [ro] = useState(
67 | () =>
68 | new ResizeObserver(() => {
69 | animationFrameId = window.requestAnimationFrame(() => {
70 | if (isCalculatingRef.current) {
71 | return;
72 | }
73 | onStart && onStart();
74 | isCalculatingRef.current = true;
75 | // `calcKey` is used in the dependencies array of
76 | // `useIsoLayoutEffect` below. It is incremented so that the font size
77 | // will be recalculated even if the previous state didn't change (e.g.
78 | // when the text fit initially).
79 | setState({
80 | ...initState(),
81 | calcKey: calcKey + 1,
82 | });
83 | });
84 | }),
85 | );
86 |
87 | useEffect(() => {
88 | if (ref.current) {
89 | ro.observe(ref.current);
90 | }
91 | return () => {
92 | animationFrameId && window.cancelAnimationFrame(animationFrameId);
93 | ro.disconnect();
94 | };
95 | }, [animationFrameId, ro]);
96 |
97 | // Recalculate when the div contents change
98 | const innerHtml = ref.current && ref.current.innerHTML;
99 | useEffect(() => {
100 | if (calcKey === 0 || isCalculatingRef.current) {
101 | return;
102 | }
103 | if (innerHtml !== innerHtmlPrevRef.current) {
104 | onStart && onStart();
105 | setState({
106 | ...initState(),
107 | calcKey: calcKey + 1,
108 | });
109 | }
110 | innerHtmlPrevRef.current = innerHtml;
111 | }, [calcKey, initState, innerHtml, onStart]);
112 |
113 | // Check overflow and resize font
114 | useIsoLayoutEffect(() => {
115 | // Don't start calculating font size until the `resizeKey` is incremented
116 | // above in the `ResizeObserver` callback. This avoids an extra resize
117 | // on initialization.
118 | if (calcKey === 0) {
119 | return;
120 | }
121 |
122 | const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;
123 | const isOverflow =
124 | !!ref.current &&
125 | (ref.current.scrollHeight > ref.current.offsetHeight ||
126 | ref.current.scrollWidth > ref.current.offsetWidth);
127 | const isFailed = isOverflow && fontSize === fontSizePrev;
128 | const isAsc = fontSize > fontSizePrev;
129 |
130 | // Return if the font size has been adjusted "enough" (change within `resolution`)
131 | // reduce font size by one increment if it's overflowing.
132 | if (isWithinResolution) {
133 | if (isFailed) {
134 | isCalculatingRef.current = false;
135 | if (logLevel <= LOG_LEVEL.info) {
136 | console.info(
137 | `[use-fit-text] reached \`minFontSize = ${minFontSize}\` without fitting text`,
138 | );
139 | }
140 | } else if (isOverflow) {
141 | setState({
142 | fontSize: isAsc ? fontSizePrev : fontSizeMin,
143 | fontSizeMax,
144 | fontSizeMin,
145 | fontSizePrev,
146 | calcKey,
147 | });
148 | } else {
149 | isCalculatingRef.current = false;
150 | onFinish && onFinish(fontSize);
151 | }
152 | return;
153 | }
154 |
155 | // Binary search to adjust font size
156 | let delta: number;
157 | let newMax = fontSizeMax;
158 | let newMin = fontSizeMin;
159 | if (isOverflow) {
160 | delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;
161 | newMax = Math.min(fontSizeMax, fontSize);
162 | } else {
163 | delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;
164 | newMin = Math.max(fontSizeMin, fontSize);
165 | }
166 | setState({
167 | calcKey,
168 | fontSize: fontSize + delta / 2,
169 | fontSizeMax: newMax,
170 | fontSizeMin: newMin,
171 | fontSizePrev: fontSize,
172 | });
173 | }, [
174 | calcKey,
175 | fontSize,
176 | fontSizeMax,
177 | fontSizeMin,
178 | fontSizePrev,
179 | onFinish,
180 | ref,
181 | resolution,
182 | ]);
183 |
184 | return { fontSize: `${fontSize}%`, ref };
185 | };
186 |
187 | export default useFitText;
188 |
--------------------------------------------------------------------------------
/dist/index.esm.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.esm.js","sources":["../index.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport ResizeObserver from \"resize-observer-polyfill\";\n\nexport type TOptions = {\n maxFontSize?: number;\n minFontSize?: number;\n onFinish?: (fontSize: number) => void;\n onStart?: () => void;\n resolution?: number;\n};\n\n// Suppress `useLayoutEffect` warning when rendering on the server\n// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" &&\n window.document &&\n window.document.createElement\n ? useLayoutEffect\n : useEffect;\n\nconst useFitText = ({\n maxFontSize = 100,\n minFontSize = 20,\n onFinish,\n onStart,\n resolution = 5,\n}: TOptions = {}) => {\n const initState = useCallback(() => {\n return {\n calcKey: 0,\n fontSize: maxFontSize,\n fontSizePrev: minFontSize,\n fontSizeMax: maxFontSize,\n fontSizeMin: minFontSize,\n };\n }, [maxFontSize, minFontSize]);\n\n const ref = useRef(null);\n const innerHtmlPrevRef = useRef();\n const isCalculatingRef = useRef(false);\n const [state, setState] = useState(initState);\n const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;\n\n // Montior div size changes and recalculate on resize\n let animationFrameId: number | null = null;\n const [ro] = useState(\n () =>\n new ResizeObserver(() => {\n animationFrameId = window.requestAnimationFrame(() => {\n if (isCalculatingRef.current) {\n return;\n }\n onStart && onStart();\n isCalculatingRef.current = true;\n // `calcKey` is used in the dependencies array of\n // `useIsoLayoutEffect` below. It is incremented so that the font size\n // will be recalculated even if the previous state didn't change (e.g.\n // when the text fit initially).\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n });\n }),\n );\n\n useEffect(() => {\n if (ref.current) {\n ro.observe(ref.current);\n }\n return () => {\n animationFrameId && window.cancelAnimationFrame(animationFrameId);\n ro.disconnect();\n };\n }, [animationFrameId, ro]);\n\n // Recalculate when the div contents change\n const innerHtml = ref.current && ref.current.innerHTML;\n useEffect(() => {\n if (calcKey === 0 || isCalculatingRef.current) {\n return;\n }\n if (innerHtml !== innerHtmlPrevRef.current) {\n onStart && onStart();\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n }\n innerHtmlPrevRef.current = innerHtml;\n }, [calcKey, initState, innerHtml, onStart]);\n\n // Check overflow and resize font\n useIsoLayoutEffect(() => {\n // Don't start calculating font size until the `resizeKey` is incremented\n // above in the `ResizeObserver` callback. This avoids an extra resize\n // on initialization.\n if (calcKey === 0) {\n return;\n }\n\n const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;\n const isOverflow =\n !!ref.current &&\n (ref.current.scrollHeight > ref.current.offsetHeight ||\n ref.current.scrollWidth > ref.current.offsetWidth);\n const isFailed = isOverflow && fontSize === fontSizePrev;\n const isAsc = fontSize > fontSizePrev;\n\n // Return if the font size has been adjusted \"enough\" (change within `resolution`)\n // reduce font size by one increment if it's overflowing.\n if (isWithinResolution) {\n if (isFailed) {\n isCalculatingRef.current = false;\n console.error(\n `Failed to fit text with \\`minFontSize = ${minFontSize}\\`. To fix, reduce \\`minFontSize\\`.`,\n );\n } else if (isOverflow) {\n setState({\n fontSize: isAsc ? fontSizePrev : fontSizeMin,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n calcKey,\n });\n } else {\n isCalculatingRef.current = false;\n onFinish && onFinish(fontSize);\n }\n return;\n }\n\n // Binary search to adjust font size\n let delta: number;\n let newMax = fontSizeMax;\n let newMin = fontSizeMin;\n if (isOverflow) {\n delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;\n newMax = Math.min(fontSizeMax, fontSize);\n } else {\n delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;\n newMin = Math.max(fontSizeMin, fontSize);\n }\n setState({\n calcKey,\n fontSize: fontSize + delta / 2,\n fontSizeMax: newMax,\n fontSizeMin: newMin,\n fontSizePrev: fontSize,\n });\n }, [\n calcKey,\n fontSize,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n onFinish,\n ref,\n resolution,\n ]);\n\n return { fontSize: `${fontSize}%`, ref };\n};\n\nexport default useFitText;\n"],"names":["useIsoLayoutEffect","window","document","createElement","useLayoutEffect","useEffect","maxFontSize","minFontSize","onFinish","onStart","resolution","initState","useCallback","calcKey","fontSize","fontSizePrev","fontSizeMax","fontSizeMin","ref","useRef","innerHtmlPrevRef","isCalculatingRef","useState","state","setState","animationFrameId","ro","ResizeObserver","requestAnimationFrame","current","observe","cancelAnimationFrame","disconnect","innerHtml","innerHTML","isWithinResolution","Math","abs","isOverflow","scrollHeight","offsetHeight","scrollWidth","offsetWidth","isAsc","console","error","delta","newMax","newMin","min","max"],"mappings":"yVAmBA,IAAMA,EACc,oBAAXC,QACPA,OAAOC,UACPD,OAAOC,SAASC,cACZC,EACAC,iBAEa,6BAML,SALZC,YAAAA,aAAc,UACdC,YAAAA,aAAc,KACdC,IAAAA,SACAC,IAAAA,YACAC,WAAAA,aAAa,IAEPC,EAAYC,EAAY,WAC5B,MAAO,CACLC,QAAS,EACTC,SAAUR,EACVS,aAAcR,EACdS,YAAaV,EACbW,YAAaV,IAEd,CAACD,EAAaC,IAEXW,EAAMC,EAAuB,MAC7BC,EAAmBD,IACnBE,EAAmBF,GAAO,KACNG,EAASX,GAA5BY,OAAOC,OACNX,EAA8DU,EAA9DV,QAASC,EAAqDS,EAArDT,SAAUE,EAA2CO,EAA3CP,YAAaC,EAA8BM,EAA9BN,YAAaF,EAAiBQ,EAAjBR,aAGjDU,EAAkC,KAC/BC,EAAMJ,EACX,sBACMK,EAAe,WACjBF,EAAmBxB,OAAO2B,sBAAsB,WAC1CP,EAAiBQ,UAGrBpB,GAAWA,IACXY,EAAiBQ,SAAU,EAK3BL,OACKb,KACHE,QAASA,EAAU,eAM7BR,EAAU,WAIR,OAHIa,EAAIW,SACNH,EAAGI,QAAQZ,EAAIW,oBAGfJ,GAAoBxB,OAAO8B,qBAAqBN,GAChDC,EAAGM,eAEJ,CAACP,EAAkBC,IAGtB,IAAMO,EAAYf,EAAIW,SAAWX,EAAIW,QAAQK,UAoF7C,OAnFA7B,EAAU,WACQ,IAAZQ,GAAiBQ,EAAiBQ,UAGlCI,IAAcb,EAAiBS,UACjCpB,GAAWA,IACXe,OACKb,KACHE,QAASA,EAAU,MAGvBO,EAAiBS,QAAUI,IAC1B,CAACpB,EAASF,EAAWsB,EAAWxB,IAGnCT,EAAmB,WAIjB,GAAgB,IAAZa,EAAJ,CAIA,IAAMsB,EAAqBC,KAAKC,IAAIvB,EAAWC,IAAiBL,EAC1D4B,IACFpB,EAAIW,UACLX,EAAIW,QAAQU,aAAerB,EAAIW,QAAQW,cACtCtB,EAAIW,QAAQY,YAAcvB,EAAIW,QAAQa,aAEpCC,EAAQ7B,EAAWC,EAIzB,GAAIoB,EALaG,GAAcxB,IAAaC,GAOxCM,EAAiBQ,SAAU,EAC3Be,QAAQC,gDACqCtC,uCAEpC+B,EACTd,EAAS,CACPV,SAAU6B,EAAQ5B,EAAeE,EACjCD,YAAAA,EACAC,YAAAA,EACAF,aAAAA,EACAF,QAAAA,KAGFQ,EAAiBQ,SAAU,EAC3BrB,GAAYA,EAASM,QAhBzB,CAsBA,IAAIgC,EACAC,EAAS/B,EACTgC,EAAS/B,EACTqB,GACFQ,EAAQH,EAAQ5B,EAAeD,EAAWG,EAAcH,EACxDiC,EAASX,KAAKa,IAAIjC,EAAaF,KAE/BgC,EAAQH,EAAQ3B,EAAcF,EAAWC,EAAeD,EACxDkC,EAASZ,KAAKc,IAAIjC,EAAaH,IAEjCU,EAAS,CACPX,QAAAA,EACAC,SAAUA,EAAWgC,EAAQ,EAC7B9B,YAAa+B,EACb9B,YAAa+B,EACbjC,aAAcD,OAEf,CACDD,EACAC,EACAE,EACAC,EACAF,EACAP,EACAU,EACAR,IAGK,CAAEI,SAAaA,MAAaI,IAAAA"}
--------------------------------------------------------------------------------
/dist/index.modern.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.modern.js","sources":["../index.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport ResizeObserver from \"resize-observer-polyfill\";\n\nexport type TOptions = {\n maxFontSize?: number;\n minFontSize?: number;\n onFinish?: (fontSize: number) => void;\n onStart?: () => void;\n resolution?: number;\n};\n\n// Suppress `useLayoutEffect` warning when rendering on the server\n// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" &&\n window.document &&\n window.document.createElement\n ? useLayoutEffect\n : useEffect;\n\nconst useFitText = ({\n maxFontSize = 100,\n minFontSize = 20,\n onFinish,\n onStart,\n resolution = 5,\n}: TOptions = {}) => {\n const initState = useCallback(() => {\n return {\n calcKey: 0,\n fontSize: maxFontSize,\n fontSizePrev: minFontSize,\n fontSizeMax: maxFontSize,\n fontSizeMin: minFontSize,\n };\n }, [maxFontSize, minFontSize]);\n\n const ref = useRef(null);\n const innerHtmlPrevRef = useRef();\n const isCalculatingRef = useRef(false);\n const [state, setState] = useState(initState);\n const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;\n\n // Montior div size changes and recalculate on resize\n let animationFrameId: number | null = null;\n const [ro] = useState(\n () =>\n new ResizeObserver(() => {\n animationFrameId = window.requestAnimationFrame(() => {\n if (isCalculatingRef.current) {\n return;\n }\n onStart && onStart();\n isCalculatingRef.current = true;\n // `calcKey` is used in the dependencies array of\n // `useIsoLayoutEffect` below. It is incremented so that the font size\n // will be recalculated even if the previous state didn't change (e.g.\n // when the text fit initially).\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n });\n }),\n );\n\n useEffect(() => {\n if (ref.current) {\n ro.observe(ref.current);\n }\n return () => {\n animationFrameId && window.cancelAnimationFrame(animationFrameId);\n ro.disconnect();\n };\n }, [animationFrameId, ro]);\n\n // Recalculate when the div contents change\n const innerHtml = ref.current && ref.current.innerHTML;\n useEffect(() => {\n if (calcKey === 0 || isCalculatingRef.current) {\n return;\n }\n if (innerHtml !== innerHtmlPrevRef.current) {\n onStart && onStart();\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n }\n innerHtmlPrevRef.current = innerHtml;\n }, [calcKey, initState, innerHtml, onStart]);\n\n // Check overflow and resize font\n useIsoLayoutEffect(() => {\n // Don't start calculating font size until the `resizeKey` is incremented\n // above in the `ResizeObserver` callback. This avoids an extra resize\n // on initialization.\n if (calcKey === 0) {\n return;\n }\n\n const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;\n const isOverflow =\n !!ref.current &&\n (ref.current.scrollHeight > ref.current.offsetHeight ||\n ref.current.scrollWidth > ref.current.offsetWidth);\n const isFailed = isOverflow && fontSize === fontSizePrev;\n const isAsc = fontSize > fontSizePrev;\n\n // Return if the font size has been adjusted \"enough\" (change within `resolution`)\n // reduce font size by one increment if it's overflowing.\n if (isWithinResolution) {\n if (isFailed) {\n isCalculatingRef.current = false;\n console.error(\n `Failed to fit text with \\`minFontSize = ${minFontSize}\\`. To fix, reduce \\`minFontSize\\`.`,\n );\n } else if (isOverflow) {\n setState({\n fontSize: isAsc ? fontSizePrev : fontSizeMin,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n calcKey,\n });\n } else {\n isCalculatingRef.current = false;\n onFinish && onFinish(fontSize);\n }\n return;\n }\n\n // Binary search to adjust font size\n let delta: number;\n let newMax = fontSizeMax;\n let newMin = fontSizeMin;\n if (isOverflow) {\n delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;\n newMax = Math.min(fontSizeMax, fontSize);\n } else {\n delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;\n newMin = Math.max(fontSizeMin, fontSize);\n }\n setState({\n calcKey,\n fontSize: fontSize + delta / 2,\n fontSizeMax: newMax,\n fontSizeMin: newMin,\n fontSizePrev: fontSize,\n });\n }, [\n calcKey,\n fontSize,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n onFinish,\n ref,\n resolution,\n ]);\n\n return { fontSize: `${fontSize}%`, ref };\n};\n\nexport default useFitText;\n"],"names":["useIsoLayoutEffect","window","document","createElement","useLayoutEffect","useEffect","maxFontSize","minFontSize","onFinish","onStart","resolution","initState","useCallback","calcKey","fontSize","fontSizePrev","fontSizeMax","fontSizeMin","ref","useRef","innerHtmlPrevRef","isCalculatingRef","state","setState","useState","animationFrameId","ro","ResizeObserver","requestAnimationFrame","current","observe","cancelAnimationFrame","disconnect","innerHtml","innerHTML","isWithinResolution","Math","abs","isOverflow","scrollHeight","offsetHeight","scrollWidth","offsetWidth","isAsc","console","error","delta","newMax","newMin","min","max"],"mappings":"0IAmBA,MAAMA,EACc,oBAAXC,QACPA,OAAOC,UACPD,OAAOC,SAASC,cACZC,EACAC,gBAEa,EACjBC,YAAAA,EAAc,IACdC,YAAAA,EAAc,GACdC,SAAAA,EACAC,QAAAA,EACAC,WAAAA,EAAa,GACD,MACZ,MAAMC,EAAYC,EAAY,KACrB,CACLC,QAAS,EACTC,SAAUR,EACVS,aAAcR,EACdS,YAAaV,EACbW,YAAaV,IAEd,CAACD,EAAaC,IAEXW,EAAMC,EAAuB,MAC7BC,EAAmBD,IACnBE,EAAmBF,GAAO,IACzBG,EAAOC,GAAYC,EAASb,IAC7BE,QAAEA,EAAFC,SAAWA,EAAXE,YAAqBA,EAArBC,YAAkCA,EAAlCF,aAA+CA,GAAiBO,EAGtE,IAAIG,EAAkC,KACtC,MAAOC,GAAMF,EACX,IACE,IAAIG,EAAe,KACjBF,EAAmBxB,OAAO2B,sBAAsB,KAC1CP,EAAiBQ,UAGrBpB,GAAWA,IACXY,EAAiBQ,SAAU,EAK3BN,EAAS,IACJZ,IACHE,QAASA,EAAU,UAM7BR,EAAU,KACJa,EAAIW,SACNH,EAAGI,QAAQZ,EAAIW,SAEV,KACLJ,GAAoBxB,OAAO8B,qBAAqBN,GAChDC,EAAGM,eAEJ,CAACP,EAAkBC,IAGtB,MAAMO,EAAYf,EAAIW,SAAWX,EAAIW,QAAQK,UAoF7C,OAnFA7B,EAAU,KACQ,IAAZQ,GAAiBQ,EAAiBQ,UAGlCI,IAAcb,EAAiBS,UACjCpB,GAAWA,IACXc,EAAS,IACJZ,IACHE,QAASA,EAAU,KAGvBO,EAAiBS,QAAUI,IAC1B,CAACpB,EAASF,EAAWsB,EAAWxB,IAGnCT,EAAmB,KAIjB,GAAgB,IAAZa,EACF,OAGF,MAAMsB,EAAqBC,KAAKC,IAAIvB,EAAWC,IAAiBL,EAC1D4B,IACFpB,EAAIW,UACLX,EAAIW,QAAQU,aAAerB,EAAIW,QAAQW,cACtCtB,EAAIW,QAAQY,YAAcvB,EAAIW,QAAQa,aAEpCC,EAAQ7B,EAAWC,EAIzB,GAAIoB,EAkBF,YAvBeG,GAAcxB,IAAaC,GAOxCM,EAAiBQ,SAAU,EAC3Be,QAAQC,iDACqCtC,yCAEpC+B,EACTf,EAAS,CACPT,SAAU6B,EAAQ5B,EAAeE,EACjCD,YAAAA,EACAC,YAAAA,EACAF,aAAAA,EACAF,QAAAA,KAGFQ,EAAiBQ,SAAU,EAC3BrB,GAAYA,EAASM,KAMzB,IAAIgC,EACAC,EAAS/B,EACTgC,EAAS/B,EACTqB,GACFQ,EAAQH,EAAQ5B,EAAeD,EAAWG,EAAcH,EACxDiC,EAASX,KAAKa,IAAIjC,EAAaF,KAE/BgC,EAAQH,EAAQ3B,EAAcF,EAAWC,EAAeD,EACxDkC,EAASZ,KAAKc,IAAIjC,EAAaH,IAEjCS,EAAS,CACPV,QAAAA,EACAC,SAAUA,EAAWgC,EAAQ,EAC7B9B,YAAa+B,EACb9B,YAAa+B,EACbjC,aAAcD,KAEf,CACDD,EACAC,EACAE,EACAC,EACAF,EACAP,EACAU,EACAR,IAGK,CAAEI,SAAaA,MAAaI,IAAAA"}
--------------------------------------------------------------------------------
/dist/index.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.js","sources":["../index.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport ResizeObserver from \"resize-observer-polyfill\";\n\nexport type TLogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nexport type TOptions = {\n logLevel?: TLogLevel;\n maxFontSize?: number;\n minFontSize?: number;\n onFinish?: (fontSize: number) => void;\n onStart?: () => void;\n resolution?: number;\n};\n\nconst LOG_LEVEL: Record = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n none: 100,\n};\n\n// Suppress `useLayoutEffect` warning when rendering on the server\n// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" &&\n window.document &&\n window.document.createElement\n ? useLayoutEffect\n : useEffect;\n\nconst useFitText = ({\n logLevel: logLevelOption = \"info\",\n maxFontSize = 100,\n minFontSize = 20,\n onFinish,\n onStart,\n resolution = 5,\n}: TOptions = {}) => {\n const logLevel = LOG_LEVEL[logLevelOption];\n\n const initState = useCallback(() => {\n return {\n calcKey: 0,\n fontSize: maxFontSize,\n fontSizePrev: minFontSize,\n fontSizeMax: maxFontSize,\n fontSizeMin: minFontSize,\n };\n }, [maxFontSize, minFontSize]);\n\n const ref = useRef(null);\n const innerHtmlPrevRef = useRef();\n const isCalculatingRef = useRef(false);\n const [state, setState] = useState(initState);\n const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;\n\n // Montior div size changes and recalculate on resize\n let animationFrameId: number | null = null;\n const [ro] = useState(\n () =>\n new ResizeObserver(() => {\n animationFrameId = window.requestAnimationFrame(() => {\n if (isCalculatingRef.current) {\n return;\n }\n onStart && onStart();\n isCalculatingRef.current = true;\n // `calcKey` is used in the dependencies array of\n // `useIsoLayoutEffect` below. It is incremented so that the font size\n // will be recalculated even if the previous state didn't change (e.g.\n // when the text fit initially).\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n });\n }),\n );\n\n useEffect(() => {\n if (ref.current) {\n ro.observe(ref.current);\n }\n return () => {\n animationFrameId && window.cancelAnimationFrame(animationFrameId);\n ro.disconnect();\n };\n }, [animationFrameId, ro]);\n\n // Recalculate when the div contents change\n const innerHtml = ref.current && ref.current.innerHTML;\n useEffect(() => {\n if (calcKey === 0 || isCalculatingRef.current) {\n return;\n }\n if (innerHtml !== innerHtmlPrevRef.current) {\n onStart && onStart();\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n }\n innerHtmlPrevRef.current = innerHtml;\n }, [calcKey, initState, innerHtml, onStart]);\n\n // Check overflow and resize font\n useIsoLayoutEffect(() => {\n // Don't start calculating font size until the `resizeKey` is incremented\n // above in the `ResizeObserver` callback. This avoids an extra resize\n // on initialization.\n if (calcKey === 0) {\n return;\n }\n\n const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;\n const isOverflow =\n !!ref.current &&\n (ref.current.scrollHeight > ref.current.offsetHeight ||\n ref.current.scrollWidth > ref.current.offsetWidth);\n const isFailed = isOverflow && fontSize === fontSizePrev;\n const isAsc = fontSize > fontSizePrev;\n\n // Return if the font size has been adjusted \"enough\" (change within `resolution`)\n // reduce font size by one increment if it's overflowing.\n if (isWithinResolution) {\n if (isFailed) {\n isCalculatingRef.current = false;\n if (logLevel <= LOG_LEVEL.info) {\n console.info(\n `[use-fit-text] reached \\`minFontSize = ${minFontSize}\\` without fitting text`,\n );\n }\n } else if (isOverflow) {\n setState({\n fontSize: isAsc ? fontSizePrev : fontSizeMin,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n calcKey,\n });\n } else {\n isCalculatingRef.current = false;\n onFinish && onFinish(fontSize);\n }\n return;\n }\n\n // Binary search to adjust font size\n let delta: number;\n let newMax = fontSizeMax;\n let newMin = fontSizeMin;\n if (isOverflow) {\n delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;\n newMax = Math.min(fontSizeMax, fontSize);\n } else {\n delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;\n newMin = Math.max(fontSizeMin, fontSize);\n }\n setState({\n calcKey,\n fontSize: fontSize + delta / 2,\n fontSizeMax: newMax,\n fontSizeMin: newMin,\n fontSizePrev: fontSize,\n });\n }, [\n calcKey,\n fontSize,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n onFinish,\n ref,\n resolution,\n ]);\n\n return { fontSize: `${fontSize}%`, ref };\n};\n\nexport default useFitText;\n"],"names":["LOG_LEVEL","debug","info","warn","error","none","useIsoLayoutEffect","window","document","createElement","useLayoutEffect","useEffect","logLevel","logLevelOption","initState","useCallback","calcKey","fontSize","maxFontSize","fontSizePrev","minFontSize","fontSizeMax","fontSizeMin","ref","useRef","innerHtmlPrevRef","isCalculatingRef","useState","animationFrameId","ResizeObserver","requestAnimationFrame","current","onStart","setState","Object","ro","observe","cancelAnimationFrame","disconnect","innerHtml","innerHTML","isWithinResolution","Math","abs","resolution","isOverflow","scrollHeight","offsetHeight","scrollWidth","offsetWidth","isAsc","console","onFinish","delta","newMax","newMin","min","max"],"mappings":"kHAoBMA,EAAuC,CAC3CC,MAAO,GACPC,KAAM,GACNC,KAAM,GACNC,MAAO,GACPC,KAAM,KAKFC,EACc,oBAAXC,QACPA,OAAOC,UACPD,OAAOC,SAASC,cACZC,kBACAC,sDASQ,oCANe,2CACb,wCACA,+DAGD,OAEPC,EAAWZ,EAAUa,GAErBC,EAAYC,+BACT,CACLC,QAAS,EACTC,SAAUC,EACVC,aAAcC,EACdC,YAAaH,EACbI,YAAaF,IAEd,CAACF,EAAaE,IAEXG,EAAMC,SAAuB,MAC7BC,EAAmBD,WACnBE,EAAmBF,UAAO,KACNG,WAASb,2FAI/Bc,EAAkC,OACzBD,6BAET,IAAIE,aACFD,EAAmBrB,OAAOuB,iCACpBJ,EAAiBK,UAGrBC,GAAWA,IACXN,EAAiBK,SAAU,EAK3BE,EAASC,iBACJpB,KACHE,QAASA,EAAU,eAM7BL,8BACMY,EAAIQ,SACNI,EAAGC,QAAQb,EAAIQ,oBAGfH,GAAoBrB,OAAO8B,qBAAqBT,GAChDO,EAAGG,eAEJ,CAACV,EAAkBO,QAGhBI,EAAYhB,EAAIQ,SAAWR,EAAIQ,QAAQS,iBAC7C7B,uBACkB,IAAZK,GAAiBU,EAAiBK,UAGlCQ,IAAcd,EAAiBM,UACjCC,GAAWA,IACXC,EAASC,iBACJpB,KACHE,QAASA,EAAU,MAGvBS,EAAiBM,QAAUQ,IAC1B,CAACvB,EAASF,EAAWyB,EAAWP,IAGnC1B,gBAIkB,IAAZU,OAIEyB,EAAqBC,KAAKC,IAAI1B,EAAWE,IAAiByB,EAC1DC,IACFtB,EAAIQ,UACLR,EAAIQ,QAAQe,aAAevB,EAAIQ,QAAQgB,cACtCxB,EAAIQ,QAAQiB,YAAczB,EAAIQ,QAAQkB,aAEpCC,EAAQjC,EAAWE,KAIrBsB,EALaI,GAAc5B,IAAaE,GAOxCO,EAAiBK,SAAU,EACvBnB,GAAYZ,EAAUE,MACxBiD,QAAQjD,8CACoCkB,6BAGrCyB,EACTZ,EAAS,CACPhB,SAAUiC,EAAQ/B,EAAeG,cACjCD,cACAC,eACAH,UACAH,KAGFU,EAAiBK,SAAU,EAC3BqB,GAAYA,EAASnC,aAMrBoC,EACAC,EAASjC,EACTkC,EAASjC,EACTuB,GACFQ,EAAQH,EAAQ/B,EAAeF,EAAWK,EAAcL,EACxDqC,EAASZ,KAAKc,IAAInC,EAAaJ,KAE/BoC,EAAQH,EAAQ7B,EAAcJ,EAAWE,EAAeF,EACxDsC,EAASb,KAAKe,IAAInC,EAAaL,IAEjCgB,EAAS,SACPjB,EACAC,SAAUA,EAAWoC,EAAQ,EAC7BhC,YAAaiC,EACbhC,YAAaiC,EACbpC,aAAcF,OAEf,CACDD,EACAC,EACAI,EACAC,EACAH,EACAiC,EACA7B,EACAqB,IAGK,CAAE3B,SAAaA,UAAaM"}
--------------------------------------------------------------------------------
/dist/index.mjs.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport ResizeObserver from \"resize-observer-polyfill\";\n\nexport type TLogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nexport type TOptions = {\n logLevel?: TLogLevel;\n maxFontSize?: number;\n minFontSize?: number;\n onFinish?: (fontSize: number) => void;\n onStart?: () => void;\n resolution?: number;\n};\n\nconst LOG_LEVEL: Record = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n none: 100,\n};\n\n// Suppress `useLayoutEffect` warning when rendering on the server\n// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" &&\n window.document &&\n window.document.createElement\n ? useLayoutEffect\n : useEffect;\n\nconst useFitText = ({\n logLevel: logLevelOption = \"info\",\n maxFontSize = 100,\n minFontSize = 20,\n onFinish,\n onStart,\n resolution = 5,\n}: TOptions = {}) => {\n const logLevel = LOG_LEVEL[logLevelOption];\n\n const initState = useCallback(() => {\n return {\n calcKey: 0,\n fontSize: maxFontSize,\n fontSizePrev: minFontSize,\n fontSizeMax: maxFontSize,\n fontSizeMin: minFontSize,\n };\n }, [maxFontSize, minFontSize]);\n\n const ref = useRef(null);\n const innerHtmlPrevRef = useRef();\n const isCalculatingRef = useRef(false);\n const [state, setState] = useState(initState);\n const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;\n\n // Montior div size changes and recalculate on resize\n let animationFrameId: number | null = null;\n const [ro] = useState(\n () =>\n new ResizeObserver(() => {\n animationFrameId = window.requestAnimationFrame(() => {\n if (isCalculatingRef.current) {\n return;\n }\n onStart && onStart();\n isCalculatingRef.current = true;\n // `calcKey` is used in the dependencies array of\n // `useIsoLayoutEffect` below. It is incremented so that the font size\n // will be recalculated even if the previous state didn't change (e.g.\n // when the text fit initially).\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n });\n }),\n );\n\n useEffect(() => {\n if (ref.current) {\n ro.observe(ref.current);\n }\n return () => {\n animationFrameId && window.cancelAnimationFrame(animationFrameId);\n ro.disconnect();\n };\n }, [animationFrameId, ro]);\n\n // Recalculate when the div contents change\n const innerHtml = ref.current && ref.current.innerHTML;\n useEffect(() => {\n if (calcKey === 0 || isCalculatingRef.current) {\n return;\n }\n if (innerHtml !== innerHtmlPrevRef.current) {\n onStart && onStart();\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n }\n innerHtmlPrevRef.current = innerHtml;\n }, [calcKey, initState, innerHtml, onStart]);\n\n // Check overflow and resize font\n useIsoLayoutEffect(() => {\n // Don't start calculating font size until the `resizeKey` is incremented\n // above in the `ResizeObserver` callback. This avoids an extra resize\n // on initialization.\n if (calcKey === 0) {\n return;\n }\n\n const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;\n const isOverflow =\n !!ref.current &&\n (ref.current.scrollHeight > ref.current.offsetHeight ||\n ref.current.scrollWidth > ref.current.offsetWidth);\n const isFailed = isOverflow && fontSize === fontSizePrev;\n const isAsc = fontSize > fontSizePrev;\n\n // Return if the font size has been adjusted \"enough\" (change within `resolution`)\n // reduce font size by one increment if it's overflowing.\n if (isWithinResolution) {\n if (isFailed) {\n isCalculatingRef.current = false;\n if (logLevel <= LOG_LEVEL.info) {\n console.info(\n `[use-fit-text] reached \\`minFontSize = ${minFontSize}\\` without fitting text`,\n );\n }\n } else if (isOverflow) {\n setState({\n fontSize: isAsc ? fontSizePrev : fontSizeMin,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n calcKey,\n });\n } else {\n isCalculatingRef.current = false;\n onFinish && onFinish(fontSize);\n }\n return;\n }\n\n // Binary search to adjust font size\n let delta: number;\n let newMax = fontSizeMax;\n let newMin = fontSizeMin;\n if (isOverflow) {\n delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;\n newMax = Math.min(fontSizeMax, fontSize);\n } else {\n delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;\n newMin = Math.max(fontSizeMin, fontSize);\n }\n setState({\n calcKey,\n fontSize: fontSize + delta / 2,\n fontSizeMax: newMax,\n fontSizeMin: newMin,\n fontSizePrev: fontSize,\n });\n }, [\n calcKey,\n fontSize,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n onFinish,\n ref,\n resolution,\n ]);\n\n return { fontSize: `${fontSize}%`, ref };\n};\n\nexport default useFitText;\n"],"names":["const","LOG_LEVEL","debug","info","warn","error","none","useIsoLayoutEffect","window","document","createElement","useLayoutEffect","useEffect","logLevel","logLevelOption","initState","useCallback","calcKey","fontSize","maxFontSize","fontSizePrev","minFontSize","fontSizeMax","fontSizeMin","ref","useRef","innerHtmlPrevRef","isCalculatingRef","useState","animationFrameId","ResizeObserver","requestAnimationFrame","current","onStart","setState","Object","ro","observe","cancelAnimationFrame","disconnect","innerHtml","innerHTML","isWithinResolution","Math","abs","resolution","isOverflow","scrollHeight","offsetHeight","scrollWidth","offsetWidth","isAsc","console","onFinish","delta","newMax","newMin","min","max"],"mappings":"0IAoBAA,IAAMC,EAAuC,CAC3CC,MAAO,GACPC,KAAM,GACNC,KAAM,GACNC,MAAO,GACPC,KAAM,KAKFC,EACc,oBAAXC,QACPA,OAAOC,UACPD,OAAOC,SAASC,cACZC,EACAC,4CASQ,oCANe,2CACb,wCACA,+DAGD,OAEPC,EAAWZ,EAAUa,GAErBC,EAAYC,mBACT,CACLC,QAAS,EACTC,SAAUC,EACVC,aAAcC,EACdC,YAAaH,EACbI,YAAaF,IAEd,CAACF,EAAaE,IAEXG,EAAMC,EAAuB,MAC7BC,EAAmBD,IACnBE,EAAmBF,GAAO,KACNG,EAASb,2FAI/Bc,EAAkC,OACzBD,oBAET,IAAIE,aACFD,EAAmBrB,OAAOuB,iCACpBJ,EAAiBK,UAGrBC,GAAWA,IACXN,EAAiBK,SAAU,EAK3BE,EAASC,iBACJpB,KACHE,QAASA,EAAU,eAM7BL,oBACMY,EAAIQ,SACNI,EAAGC,QAAQb,EAAIQ,oBAGfH,GAAoBrB,OAAO8B,qBAAqBT,GAChDO,EAAGG,eAEJ,CAACV,EAAkBO,QAGhBI,EAAYhB,EAAIQ,SAAWR,EAAIQ,QAAQS,iBAC7C7B,aACkB,IAAZK,GAAiBU,EAAiBK,UAGlCQ,IAAcd,EAAiBM,UACjCC,GAAWA,IACXC,EAASC,iBACJpB,KACHE,QAASA,EAAU,MAGvBS,EAAiBM,QAAUQ,IAC1B,CAACvB,EAASF,EAAWyB,EAAWP,IAGnC1B,gBAIkB,IAAZU,OAIEyB,EAAqBC,KAAKC,IAAI1B,EAAWE,IAAiByB,EAC1DC,IACFtB,EAAIQ,UACLR,EAAIQ,QAAQe,aAAevB,EAAIQ,QAAQgB,cACtCxB,EAAIQ,QAAQiB,YAAczB,EAAIQ,QAAQkB,aAEpCC,EAAQjC,EAAWE,KAIrBsB,EALaI,GAAc5B,IAAaE,GAOxCO,EAAiBK,SAAU,EACvBnB,GAAYZ,EAAUE,MACxBiD,QAAQjD,8CACoCkB,6BAGrCyB,EACTZ,EAAS,CACPhB,SAAUiC,EAAQ/B,EAAeG,cACjCD,cACAC,eACAH,UACAH,KAGFU,EAAiBK,SAAU,EAC3BqB,GAAYA,EAASnC,aAMrBoC,EACAC,EAASjC,EACTkC,EAASjC,EACTuB,GACFQ,EAAQH,EAAQ/B,EAAeF,EAAWK,EAAcL,EACxDqC,EAASZ,KAAKc,IAAInC,EAAaJ,KAE/BoC,EAAQH,EAAQ7B,EAAcJ,EAAWE,EAAeF,EACxDsC,EAASb,KAAKe,IAAInC,EAAaL,IAEjCgB,EAAS,SACPjB,EACAC,SAAUA,EAAWoC,EAAQ,EAC7BhC,YAAaiC,EACbhC,YAAaiC,EACbpC,aAAcF,OAEf,CACDD,EACAC,EACAI,EACAC,EACAH,EACAiC,EACA7B,EACAqB,IAGK,CAAE3B,SAAaA,UAAaM"}
--------------------------------------------------------------------------------
/dist/index.umd.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"index.umd.js","sources":["../index.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n} from \"react\";\nimport ResizeObserver from \"resize-observer-polyfill\";\n\nexport type TLogLevel = \"debug\" | \"info\" | \"warn\" | \"error\" | \"none\";\n\nexport type TOptions = {\n logLevel?: TLogLevel;\n maxFontSize?: number;\n minFontSize?: number;\n onFinish?: (fontSize: number) => void;\n onStart?: () => void;\n resolution?: number;\n};\n\nconst LOG_LEVEL: Record = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n none: 100,\n};\n\n// Suppress `useLayoutEffect` warning when rendering on the server\n// https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85\nconst useIsoLayoutEffect =\n typeof window !== \"undefined\" &&\n window.document &&\n window.document.createElement\n ? useLayoutEffect\n : useEffect;\n\nconst useFitText = ({\n logLevel: logLevelOption = \"info\",\n maxFontSize = 100,\n minFontSize = 20,\n onFinish,\n onStart,\n resolution = 5,\n}: TOptions = {}) => {\n const logLevel = LOG_LEVEL[logLevelOption];\n\n const initState = useCallback(() => {\n return {\n calcKey: 0,\n fontSize: maxFontSize,\n fontSizePrev: minFontSize,\n fontSizeMax: maxFontSize,\n fontSizeMin: minFontSize,\n };\n }, [maxFontSize, minFontSize]);\n\n const ref = useRef(null);\n const innerHtmlPrevRef = useRef();\n const isCalculatingRef = useRef(false);\n const [state, setState] = useState(initState);\n const { calcKey, fontSize, fontSizeMax, fontSizeMin, fontSizePrev } = state;\n\n // Montior div size changes and recalculate on resize\n let animationFrameId: number | null = null;\n const [ro] = useState(\n () =>\n new ResizeObserver(() => {\n animationFrameId = window.requestAnimationFrame(() => {\n if (isCalculatingRef.current) {\n return;\n }\n onStart && onStart();\n isCalculatingRef.current = true;\n // `calcKey` is used in the dependencies array of\n // `useIsoLayoutEffect` below. It is incremented so that the font size\n // will be recalculated even if the previous state didn't change (e.g.\n // when the text fit initially).\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n });\n }),\n );\n\n useEffect(() => {\n if (ref.current) {\n ro.observe(ref.current);\n }\n return () => {\n animationFrameId && window.cancelAnimationFrame(animationFrameId);\n ro.disconnect();\n };\n }, [animationFrameId, ro]);\n\n // Recalculate when the div contents change\n const innerHtml = ref.current && ref.current.innerHTML;\n useEffect(() => {\n if (calcKey === 0 || isCalculatingRef.current) {\n return;\n }\n if (innerHtml !== innerHtmlPrevRef.current) {\n onStart && onStart();\n setState({\n ...initState(),\n calcKey: calcKey + 1,\n });\n }\n innerHtmlPrevRef.current = innerHtml;\n }, [calcKey, initState, innerHtml, onStart]);\n\n // Check overflow and resize font\n useIsoLayoutEffect(() => {\n // Don't start calculating font size until the `resizeKey` is incremented\n // above in the `ResizeObserver` callback. This avoids an extra resize\n // on initialization.\n if (calcKey === 0) {\n return;\n }\n\n const isWithinResolution = Math.abs(fontSize - fontSizePrev) <= resolution;\n const isOverflow =\n !!ref.current &&\n (ref.current.scrollHeight > ref.current.offsetHeight ||\n ref.current.scrollWidth > ref.current.offsetWidth);\n const isFailed = isOverflow && fontSize === fontSizePrev;\n const isAsc = fontSize > fontSizePrev;\n\n // Return if the font size has been adjusted \"enough\" (change within `resolution`)\n // reduce font size by one increment if it's overflowing.\n if (isWithinResolution) {\n if (isFailed) {\n isCalculatingRef.current = false;\n if (logLevel <= LOG_LEVEL.info) {\n console.info(\n `[use-fit-text] reached \\`minFontSize = ${minFontSize}\\` without fitting text`,\n );\n }\n } else if (isOverflow) {\n setState({\n fontSize: isAsc ? fontSizePrev : fontSizeMin,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n calcKey,\n });\n } else {\n isCalculatingRef.current = false;\n onFinish && onFinish(fontSize);\n }\n return;\n }\n\n // Binary search to adjust font size\n let delta: number;\n let newMax = fontSizeMax;\n let newMin = fontSizeMin;\n if (isOverflow) {\n delta = isAsc ? fontSizePrev - fontSize : fontSizeMin - fontSize;\n newMax = Math.min(fontSizeMax, fontSize);\n } else {\n delta = isAsc ? fontSizeMax - fontSize : fontSizePrev - fontSize;\n newMin = Math.max(fontSizeMin, fontSize);\n }\n setState({\n calcKey,\n fontSize: fontSize + delta / 2,\n fontSizeMax: newMax,\n fontSizeMin: newMin,\n fontSizePrev: fontSize,\n });\n }, [\n calcKey,\n fontSize,\n fontSizeMax,\n fontSizeMin,\n fontSizePrev,\n onFinish,\n ref,\n resolution,\n ]);\n\n return { fontSize: `${fontSize}%`, ref };\n};\n\nexport default useFitText;\n"],"names":["const","LOG_LEVEL","debug","info","warn","error","none","useIsoLayoutEffect","window","document","createElement","useLayoutEffect","useEffect","logLevel","logLevelOption","initState","useCallback","calcKey","fontSize","maxFontSize","fontSizePrev","minFontSize","fontSizeMax","fontSizeMin","ref","useRef","innerHtmlPrevRef","isCalculatingRef","useState","animationFrameId","ResizeObserver","requestAnimationFrame","current","onStart","setState","Object","ro","observe","cancelAnimationFrame","disconnect","innerHtml","innerHTML","isWithinResolution","Math","abs","resolution","isOverflow","scrollHeight","offsetHeight","scrollWidth","offsetWidth","isAsc","console","onFinish","delta","newMax","newMin","min","max"],"mappings":"0UAoBAA,IAAMC,EAAuC,CAC3CC,MAAO,GACPC,KAAM,GACNC,KAAM,GACNC,MAAO,GACPC,KAAM,KAKFC,EACc,oBAAXC,QACPA,OAAOC,UACPD,OAAOC,SAASC,cACZC,kBACAC,8CASQ,oCANe,2CACb,wCACA,+DAGD,OAEPC,EAAWZ,EAAUa,GAErBC,EAAYC,+BACT,CACLC,QAAS,EACTC,SAAUC,EACVC,aAAcC,EACdC,YAAaH,EACbI,YAAaF,IAEd,CAACF,EAAaE,IAEXG,EAAMC,SAAuB,MAC7BC,EAAmBD,WACnBE,EAAmBF,UAAO,KACNG,WAASb,2FAI/Bc,EAAkC,OACzBD,6BAET,IAAIE,aACFD,EAAmBrB,OAAOuB,iCACpBJ,EAAiBK,UAGrBC,GAAWA,IACXN,EAAiBK,SAAU,EAK3BE,EAASC,iBACJpB,KACHE,QAASA,EAAU,eAM7BL,8BACMY,EAAIQ,SACNI,EAAGC,QAAQb,EAAIQ,oBAGfH,GAAoBrB,OAAO8B,qBAAqBT,GAChDO,EAAGG,eAEJ,CAACV,EAAkBO,QAGhBI,EAAYhB,EAAIQ,SAAWR,EAAIQ,QAAQS,iBAC7C7B,uBACkB,IAAZK,GAAiBU,EAAiBK,UAGlCQ,IAAcd,EAAiBM,UACjCC,GAAWA,IACXC,EAASC,iBACJpB,KACHE,QAASA,EAAU,MAGvBS,EAAiBM,QAAUQ,IAC1B,CAACvB,EAASF,EAAWyB,EAAWP,IAGnC1B,gBAIkB,IAAZU,OAIEyB,EAAqBC,KAAKC,IAAI1B,EAAWE,IAAiByB,EAC1DC,IACFtB,EAAIQ,UACLR,EAAIQ,QAAQe,aAAevB,EAAIQ,QAAQgB,cACtCxB,EAAIQ,QAAQiB,YAAczB,EAAIQ,QAAQkB,aAEpCC,EAAQjC,EAAWE,KAIrBsB,EALaI,GAAc5B,IAAaE,GAOxCO,EAAiBK,SAAU,EACvBnB,GAAYZ,EAAUE,MACxBiD,QAAQjD,8CACoCkB,6BAGrCyB,EACTZ,EAAS,CACPhB,SAAUiC,EAAQ/B,EAAeG,cACjCD,cACAC,eACAH,UACAH,KAGFU,EAAiBK,SAAU,EAC3BqB,GAAYA,EAASnC,aAMrBoC,EACAC,EAASjC,EACTkC,EAASjC,EACTuB,GACFQ,EAAQH,EAAQ/B,EAAeF,EAAWK,EAAcL,EACxDqC,EAASZ,KAAKc,IAAInC,EAAaJ,KAE/BoC,EAAQH,EAAQ7B,EAAcJ,EAAWE,EAAeF,EACxDsC,EAASb,KAAKe,IAAInC,EAAaL,IAEjCgB,EAAS,SACPjB,EACAC,SAAUA,EAAWoC,EAAQ,EAC7BhC,YAAaiC,EACbhC,YAAaiC,EACbpC,aAAcF,OAEf,CACDD,EACAC,EACAI,EACAC,EACAH,EACAiC,EACA7B,EACAqB,IAGK,CAAE3B,SAAaA,UAAaM"}
--------------------------------------------------------------------------------