Skip to content

Commit

Permalink
Fixed an issue with resize not happening after form being reset
Browse files Browse the repository at this point in the history
  • Loading branch information
Andarist committed Jan 9, 2025
1 parent ae64b9f commit 9efe706
Show file tree
Hide file tree
Showing 8 changed files with 4,552 additions and 8,988 deletions.
2 changes: 1 addition & 1 deletion example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@
<body>
<h1>React &lt;TextareaAutosize /&gt; component</h1>
<div id="main"></div>
<script src="./index.tsx"></script>
<script type="module" src="./index.tsx"></script>
</body>
21 changes: 19 additions & 2 deletions example/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import TextareaAutosize from '../src';

const range = (n: number): number[] => Array.from({ length: n }, (_, i) => i);
Expand Down Expand Up @@ -202,6 +202,22 @@ const WithCustomFont = () => {
);
};

const WithFormReset = () => {
const ref = React.useRef<HTMLFormElement>(null);
return (
<div>
<h2>{'Resettable form via manual reset call.'}</h2>
<div>{'Resizes once the form gets reset.'}</div>
<form ref={ref}>
<TextareaAutosize />
<button type="button" onClick={() => ref.current?.reset()}>
{'Reset'}
</button>
</form>
</div>
);
};

const Demo = () => {
return (
<div>
Expand All @@ -215,8 +231,9 @@ const Demo = () => {
<OnHeightChangeCallback />
<MultipleTextareas />
<WithCustomFont />
<WithFormReset />
</div>
);
};

ReactDOM.render(<Demo />, document.getElementById('main'));
createRoot(document.getElementById('main')!).render(<Demo />);
5 changes: 3 additions & 2 deletions example/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "parcel ./index.html --open",
"build": "parcel build ./index.html --dist-dir ./dist --public-url ."
"dev": "vite",
"build": "vite build"
}
}
6 changes: 6 additions & 0 deletions example/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
});
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,10 @@
"Mateusz Burzyński <[email protected]> (https://github.com/Andarist)"
],
"scripts": {
"prebuild": "npm run clean",
"build": "preconstruct build",
"docs:dev": "npm run dev --prefix example",
"docs:build": "npm run build --prefix example",
"docs:publish": "npm run docs:build && cd ./example/dist && git init && git commit --allow-empty -m 'update docs' && git checkout -b gh-pages && touch .nojekyll && git add . && git commit -am 'update docs' && git push [email protected]:Andarist/react-textarea-autosize gh-pages --force",
"clean": "rimraf dist",
"lint": "eslint --ext .js,.ts,.tsx src",
"prepare": "npm run build",
"changeset": "changeset",
Expand Down Expand Up @@ -110,10 +108,11 @@
"@preconstruct/cli": "^2.8.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^10.4.9",
"@types/react": "^16.14.35",
"@types/react-dom": "^16.9.17",
"@types/react": "^18",
"@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^5.51.0",
"@typescript-eslint/parser": "^5.51.0",
"@vitejs/plugin-react": "^4.3.4",
"babel-eslint": "11.0.0-beta.2",
"bytes": "^3.1.0",
"cross-env": "^7.0.2",
Expand All @@ -125,13 +124,12 @@
"jest": "^29.4.2",
"jest-environment-jsdom": "^29.4.2",
"lint-staged": "^10.2.8",
"parcel": "2.0.0-nightly.454",
"prettier": "^2.8.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"terser": "^4.7.0",
"typescript": "^5.1.3"
"typescript": "^5.1.3",
"vite": "^6.0.7"
},
"engines": {
"node": ">=10"
Expand Down
28 changes: 20 additions & 8 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,43 @@ type InferEvent<
: Event;

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

// might happen if document.fonts is not defined, for instance
if (!target) {
if (!resolvedTarget) {
return;
}

target.addEventListener(type, handler);
return () => target.removeEventListener(type, handler);
resolvedTarget.addEventListener(type, handler);
return () => resolvedTarget.removeEventListener(type, handler);
}, []);
}

export const useFormResetListener = (
libRef: React.MutableRefObject<HTMLTextAreaElement | null>,
listener: (event: Event) => any,
) => {
useListener(() => libRef.current?.form, 'reset', listener);
};

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

export const useFontsLoadedListener = (listener: (event: Event) => any) => {
useListener(document.fonts, 'loadingdone', listener);
};

export const useForceRerender = () => {
const [, setState] = React.useState({});
return React.useCallback(() => setState({}), []);
};
20 changes: 20 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
useComposedRef,
useWindowResizeListener,
useFontsLoadedListener,
useFormResetListener,
useForceRerender,
} from './hooks';
import { noop } from './utils';

Expand Down Expand Up @@ -97,7 +99,25 @@ const TextareaAutosize: React.ForwardRefRenderFunction<
};

if (isBrowser) {
const forceRerender = useForceRerender();
React.useLayoutEffect(resizeTextarea);
useFormResetListener(libRef, () => {
if (!isControlled) {
// force rerender is used here because form reset doesn't trigger React's onChange:
// https://github.com/facebook/react/issues/19078
//
// the problem with a reset listener is that it's called before the value gets actually changed
// the event itself can, after all, be even .preventDefault()ed
// so given it's not possible to know if the reset will actually happen, we "schedule" a rerender so our resizing layout effect can take care of it
//
// this doesn't work with <input type="reset" /> though
// updates scheduled by reset handlers called called by those happen synchronously~
// React is eager to rerender this before the reset action actually takes place
//
// it might be a good idea to use a native change listener on the textarea itself to workaround this
forceRerender();
}
});
useWindowResizeListener(resizeTextarea);
useFontsLoadedListener(resizeTextarea);
return <textarea {...props} onChange={handleChange} ref={ref} />;
Expand Down
Loading

0 comments on commit 9efe706

Please sign in to comment.