use-fit-text
Version:
React hook used to fit text in a div
1 lines • 7.69 kB
Source Map (JSON)
{"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<TLogLevel, number> = {\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<HTMLDivElement>(null);\n const innerHtmlPrevRef = useRef<string>();\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"}