Skip to content

Commit

Permalink
Handle custom fonts loading (#362)
Browse files Browse the repository at this point in the history
* Add a breaking example with custom font

* Trigger resizer when all fonts are loaded

* Factorize event listener hooks

* Upgrade dev dependencies to work with latest TS (4.9.5)

* Add JSDom and mock document.fonts listner methods

* Update .changeset/tricky-jokes-wave.md

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
ArnaudRinquin and Andarist committed Mar 21, 2023
1 parent 0d82f52 commit 2301195
Show file tree
Hide file tree
Showing 9 changed files with 4,147 additions and 1,396 deletions.
5 changes: 5 additions & 0 deletions .changeset/tricky-jokes-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-textarea-autosize': patch
---

Support automatic resizing when a custom fonts ends up loading
6 changes: 6 additions & 0 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
}
</style>
<title>React &lt;TextareaAutosize /&gt; component</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@800&display=swap"
rel="stylesheet"
/>
</head>
<body>
<h1>React &lt;TextareaAutosize /&gt; component</h1>
Expand Down
28 changes: 24 additions & 4 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const ControlledMode = () => {
<TextareaAutosize
cacheMeasurements
value={value}
onChange={ev => setValue(ev.target.value)}
onChange={(ev) => setValue(ev.target.value)}
/>
<button onClick={() => setValue('This value was set programatically')}>
{'Change value programatically'}
Expand Down Expand Up @@ -156,7 +156,7 @@ const OnHeightChangeCallback = () => {
</pre>
<TextareaAutosize
cacheMeasurements
onHeightChange={height => {
onHeightChange={(height) => {
// eslint-disable-next-line no-console
console.log(height);
}}
Expand All @@ -173,16 +173,35 @@ const MultipleTextareas = () => {
<div>{'This one controls the rest.'}</div>
<TextareaAutosize
value={value}
onChange={ev => setValue(ev.target.value)}
onChange={(ev) => setValue(ev.target.value)}
/>
<div>{'Those get controlled by the one above.'}</div>
{range(15).map(i => (
{range(15).map((i) => (
<TextareaAutosize key={i} value={value} />
))}
</div>
);
};

const WithCustomFont = () => {
return (
<div>
<h2>{'Adapts to custom fonts.'}</h2>
<div>{'Resizes once the font is loaded.'}</div>
<TextareaAutosize
style={{
fontSize: 20,
fontFamily: "'Work Sans', sans-serif",
}}
defaultValue={'The quick brown fox jumps over the lazy dog'}
onHeightChange={(rows) => {
console.log('onChange', rows);
}}
/>
</div>
);
};

const Demo = () => {
return (
<div>
Expand All @@ -195,6 +214,7 @@ const Demo = () => {
<UncontrolledMode />
<OnHeightChangeCallback />
<MultipleTextareas />
<WithCustomFont />
</div>
);
};
Expand Down
41 changes: 21 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,43 +57,44 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"dependencies": {
"@babel/runtime": "^7.10.2",
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",
"use-latest": "^1.2.1"
},
"devDependencies": {
"@babel/core": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.4",
"@babel/preset-env": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/core": "^7.20.12",
"@babel/plugin-proposal-object-rest-spread": "^7.20.7",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@changesets/changelog-github": "^0.4.4",
"@changesets/cli": "^2.22.0",
"@preconstruct/cli": "^2.2.2",
"@testing-library/jest-dom": "^5.9.0",
"@testing-library/react": "^10.1.0",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^3.1.0",
"@typescript-eslint/parser": "^3.1.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^10.4.9",
"@types/react": "^16.14.35",
"@types/react-dom": "^16.9.17",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"babel-eslint": "11.0.0-beta.2",
"bytes": "^3.1.0",
"cross-env": "^7.0.2",
"eslint": "^7.1.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.20.0",
"eslint": "^8.33.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"husky": "^4.2.5",
"jest": "^26.0.1",
"jest": "^29.4.2",
"jest-environment-jsdom": "^29.4.2",
"lint-staged": "^10.2.8",
"parcel": "2.0.0-nightly.454",
"prettier": "^2.0.5",
"prettier": "^2.8.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"rimraf": "^3.0.2",
"terser": "^4.7.0",
"typescript": "^3.9.3"
"typescript": "^4.9.5"
},
"engines": {
"node": ">=10"
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
/**
* @jest-environment jsdom
*/
import '@testing-library/jest-dom/extend-expect';
import * as React from 'react';
import { render } from '@testing-library/react';
import TextareaAutosize from '../index';

describe('<TextareaAutosize />', () => {
beforeAll(() => {
// Unfortunately, JSDom does not implement document.fonts yet
// so we're mocking it
Object.defineProperty(document, 'fonts', {
value: { addEventListener() {}, removeEventListener() {} },
});
});
it('renders ok', () => {
const { asFragment } = render(<TextareaAutosize />);

Expand Down
4 changes: 2 additions & 2 deletions src/getSizingData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const SIZING_STYLE = [
] as const;

type SizingProps = Extract<
typeof SIZING_STYLE[number],
(typeof SIZING_STYLE)[number],
keyof CSSStyleDeclaration
>;

Expand All @@ -51,7 +51,7 @@ const getSizingData = (node: HTMLElement): SizingData | null => {
return null;
}

const sizingStyle = pick((SIZING_STYLE as unknown) as SizingProps[], style);
const sizingStyle = pick(SIZING_STYLE as unknown as SizingProps[], style);
const { boxSizing } = sizingStyle;

// probably node is detached from DOM, can't read computed dimensions
Expand Down
50 changes: 40 additions & 10 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,48 @@ import useLatest from 'use-latest';

export { default as useComposedRef } from 'use-composed-ref';

export const useWindowResizeListener = (listener: (event: UIEvent) => any) => {
const latestListener = useLatest(listener);
type UnknownFunction = (...args: any[]) => any;

React.useLayoutEffect(() => {
const handler: typeof listener = (event) => {
latestListener.current(event);
};
type InferEventType<TTarget> = TTarget extends {
// we infer from 2 overloads which are super common for event targets in the DOM lib
// we "prioritize" the first one as the first one is always more specific
addEventListener(type: infer P, ...args: any): void;
// we can ignore the second one as it's usually just a fallback that allows bare `string` here
// we use `infer P2` over `any` as we really don't care about this type value
// and we don't want to accidentally fail a type assignability check, remember that `any` isn't assignable to `never`
addEventListener(type: infer P2, ...args: any): void;
}
? P & string
: never;

window.addEventListener('resize', handler);
type InferEvent<
TTarget,
TType extends string,
> = `on${TType}` extends keyof TTarget
? Parameters<Extract<TTarget[`on${TType}`], UnknownFunction>>[0]
: Event;

function useListener<
TTarget extends EventTarget,
TType extends InferEventType<TTarget>,
>(
target: TTarget,
type: TType,
listener: (event: InferEvent<TTarget, TType>) => void,
) {
const latestListener = useLatest(listener);
React.useLayoutEffect(() => {
const handler: typeof listener = (ev) => latestListener.current(ev);

return () => {
window.removeEventListener('resize', handler);
};
target.addEventListener(type, handler);
return () => target.removeEventListener(type, handler);
}, []);
}

export const useWindowResizeListener = (listener: (event: UIEvent) => any) => {
useListener(window, 'resize', listener);
};

export const useFontsLoadedListener = (listener: (event: Event) => any) => {
useListener(document.fonts, 'loadingdone', listener);
};
7 changes: 6 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import calculateNodeHeight from './calculateNodeHeight';
import getSizingData, { SizingData } from './getSizingData';
import { useComposedRef, useWindowResizeListener } from './hooks';
import {
useComposedRef,
useWindowResizeListener,
useFontsLoadedListener,
} from './hooks';
import { noop } from './utils';

type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
Expand Down Expand Up @@ -93,6 +97,7 @@ const TextareaAutosize: React.ForwardRefRenderFunction<
if (typeof document !== 'undefined') {
React.useLayoutEffect(resizeTextarea);
useWindowResizeListener(resizeTextarea);
useFontsLoadedListener(resizeTextarea);
}

return <textarea {...props} onChange={handleChange} ref={ref} />;
Expand Down
Loading

0 comments on commit 2301195

Please sign in to comment.