Skip to content

Commit 9efe706

Browse files
committed
Fixed an issue with resize not happening after form being reset
1 parent ae64b9f commit 9efe706

File tree

8 files changed

+4552
-8988
lines changed

8 files changed

+4552
-8988
lines changed

example/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@
2323
<body>
2424
<h1>React &lt;TextareaAutosize /&gt; component</h1>
2525
<div id="main"></div>
26-
<script src="./index.tsx"></script>
26+
<script type="module" src="./index.tsx"></script>
2727
</body>

example/index.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import * as ReactDOM from 'react-dom';
2+
import { createRoot } from 'react-dom/client';
33
import TextareaAutosize from '../src';
44

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

205+
const WithFormReset = () => {
206+
const ref = React.useRef<HTMLFormElement>(null);
207+
return (
208+
<div>
209+
<h2>{'Resettable form via manual reset call.'}</h2>
210+
<div>{'Resizes once the form gets reset.'}</div>
211+
<form ref={ref}>
212+
<TextareaAutosize />
213+
<button type="button" onClick={() => ref.current?.reset()}>
214+
{'Reset'}
215+
</button>
216+
</form>
217+
</div>
218+
);
219+
};
220+
205221
const Demo = () => {
206222
return (
207223
<div>
@@ -215,8 +231,9 @@ const Demo = () => {
215231
<OnHeightChangeCallback />
216232
<MultipleTextareas />
217233
<WithCustomFont />
234+
<WithFormReset />
218235
</div>
219236
);
220237
};
221238

222-
ReactDOM.render(<Demo />, document.getElementById('main'));
239+
createRoot(document.getElementById('main')!).render(<Demo />);

example/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"private": true,
3+
"type": "module",
34
"scripts": {
4-
"dev": "parcel ./index.html --open",
5-
"build": "parcel build ./index.html --dist-dir ./dist --public-url ."
5+
"dev": "vite",
6+
"build": "vite build"
67
}
78
}

example/vite.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from 'vite';
2+
import react from '@vitejs/plugin-react';
3+
4+
export default defineConfig({
5+
plugins: [react()],
6+
});

package.json

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,10 @@
7676
"Mateusz Burzyński <[email protected]> (https://github.com/Andarist)"
7777
],
7878
"scripts": {
79-
"prebuild": "npm run clean",
8079
"build": "preconstruct build",
8180
"docs:dev": "npm run dev --prefix example",
8281
"docs:build": "npm run build --prefix example",
8382
"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",
84-
"clean": "rimraf dist",
8583
"lint": "eslint --ext .js,.ts,.tsx src",
8684
"prepare": "npm run build",
8785
"changeset": "changeset",
@@ -110,10 +108,11 @@
110108
"@preconstruct/cli": "^2.8.1",
111109
"@testing-library/jest-dom": "^5.16.5",
112110
"@testing-library/react": "^10.4.9",
113-
"@types/react": "^16.14.35",
114-
"@types/react-dom": "^16.9.17",
111+
"@types/react": "^18",
112+
"@types/react-dom": "^18",
115113
"@typescript-eslint/eslint-plugin": "^5.51.0",
116114
"@typescript-eslint/parser": "^5.51.0",
115+
"@vitejs/plugin-react": "^4.3.4",
117116
"babel-eslint": "11.0.0-beta.2",
118117
"bytes": "^3.1.0",
119118
"cross-env": "^7.0.2",
@@ -125,13 +124,12 @@
125124
"jest": "^29.4.2",
126125
"jest-environment-jsdom": "^29.4.2",
127126
"lint-staged": "^10.2.8",
128-
"parcel": "2.0.0-nightly.454",
129127
"prettier": "^2.8.4",
130-
"react": "^16.13.1",
131-
"react-dom": "^16.13.1",
128+
"react": "^18.2.0",
129+
"react-dom": "^18.2.0",
132130
"rimraf": "^3.0.2",
133-
"terser": "^4.7.0",
134-
"typescript": "^5.1.3"
131+
"typescript": "^5.1.3",
132+
"vite": "^6.0.7"
135133
},
136134
"engines": {
137135
"node": ">=10"

src/hooks.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,43 @@ type InferEvent<
2525
: Event;
2626

2727
function useListener<
28-
TTarget extends EventTarget,
29-
TType extends InferEventType<TTarget>,
28+
TTarget extends EventTarget | null | undefined,
29+
TType extends InferEventType<NonNullable<TTarget>>,
3030
>(
31-
target: TTarget,
31+
target: (() => TTarget) | TTarget,
3232
type: TType,
33-
listener: (event: InferEvent<TTarget, TType>) => void,
33+
listener: (event: InferEvent<NonNullable<TTarget>, TType>) => void,
3434
) {
3535
const latestListener = useLatest(listener);
3636
React.useLayoutEffect(() => {
3737
const handler: typeof listener = (ev) => latestListener.current(ev);
38+
const resolvedTarget = typeof target === 'function' ? target() : target;
3839

3940
// might happen if document.fonts is not defined, for instance
40-
if (!target) {
41+
if (!resolvedTarget) {
4142
return;
4243
}
43-
44-
target.addEventListener(type, handler);
45-
return () => target.removeEventListener(type, handler);
44+
resolvedTarget.addEventListener(type, handler);
45+
return () => resolvedTarget.removeEventListener(type, handler);
4646
}, []);
4747
}
4848

49+
export const useFormResetListener = (
50+
libRef: React.MutableRefObject<HTMLTextAreaElement | null>,
51+
listener: (event: Event) => any,
52+
) => {
53+
useListener(() => libRef.current?.form, 'reset', listener);
54+
};
55+
4956
export const useWindowResizeListener = (listener: (event: UIEvent) => any) => {
5057
useListener(window, 'resize', listener);
5158
};
5259

5360
export const useFontsLoadedListener = (listener: (event: Event) => any) => {
5461
useListener(document.fonts, 'loadingdone', listener);
5562
};
63+
64+
export const useForceRerender = () => {
65+
const [, setState] = React.useState({});
66+
return React.useCallback(() => setState({}), []);
67+
};

src/index.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
useComposedRef,
88
useWindowResizeListener,
99
useFontsLoadedListener,
10+
useFormResetListener,
11+
useForceRerender,
1012
} from './hooks';
1113
import { noop } from './utils';
1214

@@ -97,7 +99,25 @@ const TextareaAutosize: React.ForwardRefRenderFunction<
9799
};
98100

99101
if (isBrowser) {
102+
const forceRerender = useForceRerender();
100103
React.useLayoutEffect(resizeTextarea);
104+
useFormResetListener(libRef, () => {
105+
if (!isControlled) {
106+
// force rerender is used here because form reset doesn't trigger React's onChange:
107+
// https://github.com/facebook/react/issues/19078
108+
//
109+
// the problem with a reset listener is that it's called before the value gets actually changed
110+
// the event itself can, after all, be even .preventDefault()ed
111+
// 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
112+
//
113+
// this doesn't work with <input type="reset" /> though
114+
// updates scheduled by reset handlers called called by those happen synchronously~
115+
// React is eager to rerender this before the reset action actually takes place
116+
//
117+
// it might be a good idea to use a native change listener on the textarea itself to workaround this
118+
forceRerender();
119+
}
120+
});
101121
useWindowResizeListener(resizeTextarea);
102122
useFontsLoadedListener(resizeTextarea);
103123
return <textarea {...props} onChange={handleChange} ref={ref} />;

0 commit comments

Comments
 (0)