@octanejs/textarea-autosize 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +25 -0
- package/UPSTREAM.md +33 -0
- package/package.json +134 -0
- package/src/calculateNodeHeight.ts +38 -0
- package/src/conditions/false.ts +1 -0
- package/src/conditions/is-browser.ts +1 -0
- package/src/conditions/true.ts +1 -0
- package/src/forceHiddenStyles.ts +22 -0
- package/src/getSizingData.ts +48 -0
- package/src/hooks.ts +52 -0
- package/src/index.tsrx +111 -0
- package/src/utils.ts +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Andrey Popp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @octanejs/textarea-autosize
|
|
2
|
+
|
|
3
|
+
Octane binding for `react-textarea-autosize@8.5.9`.
|
|
4
|
+
|
|
5
|
+
```tsrx
|
|
6
|
+
import TextareaAutosize from '@octanejs/textarea-autosize';
|
|
7
|
+
|
|
8
|
+
export function Notes() @{
|
|
9
|
+
<TextareaAutosize minRows={2} maxRows={8} />
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The binding preserves the default component and named `TextareaAutosizeProps` and
|
|
14
|
+
`TextareaHeightChangeMeta` types from `react-textarea-autosize@8.5.9`. Native
|
|
15
|
+
textarea attributes, `minRows`, `maxRows`, `cacheMeasurements`, refs, form reset,
|
|
16
|
+
font loading, window resizing, SSR, and `onHeightChange` are supported.
|
|
17
|
+
|
|
18
|
+
The public `onChange` callback runs for user edits after uncontrolled sizing, as it
|
|
19
|
+
does upstream. Octane passes the native `InputEvent` rather than a React
|
|
20
|
+
`SyntheticEvent`; use `event.currentTarget.value` during dispatch. Assigning the
|
|
21
|
+
DOM `value` property programmatically does not synthesize either framework's
|
|
22
|
+
change callback—dispatch an input event when testing that path.
|
|
23
|
+
|
|
24
|
+
`style.minHeight` and `style.maxHeight` are rejected in development. Use
|
|
25
|
+
`minRows` and `maxRows` instead.
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# textarea-autosize upstream evidence
|
|
2
|
+
|
|
3
|
+
- Package: `react-textarea-autosize@8.5.9`
|
|
4
|
+
- npm tarball SHA-1: `ab8627b09aa04d8a2f45d5b5cd94c84d1d4a8893`
|
|
5
|
+
- npm tarball SHA-256: `648a3eae6d4a8708e215b7272907ba018b5f77430acbb568a2c5aebd238ce945`
|
|
6
|
+
- npm integrity: `sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==`
|
|
7
|
+
- Source commit: `ed1894cd8611d99fbea1c47adcf6ee522b1030fd`
|
|
8
|
+
- License: MIT, copyright 2013 Andrey Popp
|
|
9
|
+
- React oracle: `react@19.2.7`, `react-dom@19.2.7`, `@types/react@19.2.17`, and `@types/react-dom@19.2.3` via catalog `react-textarea-autosize-react-oracle` (exact pins; not `catalog:default`)
|
|
10
|
+
- Published declarations oracle: `upstream/dist/declarations/src/index.d.ts` (sha256 `4b44a78900c844368d8f27ce485bb55bd17ba164cb31e3b8bbc64c6800da506c`), extracted byte-exact from the pinned npm tarball for the pristine type lane
|
|
11
|
+
|
|
12
|
+
The npm artifact supplies the published distribution and package-condition boundary. The exact repository commit supplies the source and two upstream Jest artifacts absent from the tarball. `upstream/` is byte-exact development evidence and is excluded from publication.
|
|
13
|
+
|
|
14
|
+
## Public surface crosswalk
|
|
15
|
+
|
|
16
|
+
| Upstream surface | Octane surface | Status | Evidence |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| default `TextareaAutosize` | default `TextareaAutosize` | Ported | runtime, SSR, hydration, and browser lanes |
|
|
19
|
+
| `TextareaAutosizeProps` | `TextareaAutosizeProps` | Ported with native event types | paired type lanes |
|
|
20
|
+
| `TextareaHeightChangeMeta` | `TextareaHeightChangeMeta` | Ported | paired type lanes |
|
|
21
|
+
| root conditional exports | root source export with matching conditions | Ported | packed resolver matrix |
|
|
22
|
+
| `./package.json` | `./package.json` | Ported | packed resolver matrix |
|
|
23
|
+
|
|
24
|
+
## Test disposition
|
|
25
|
+
|
|
26
|
+
| Upstream artifact | Disposition |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `src/__tests__/index.test.js` | Run pristine with React and adapted case-for-case under `tests/upstream/` |
|
|
29
|
+
| `src/__tests__/__snapshots__/index.test.js.snap` | Retained pristine; equivalent adapted DOM assertions avoid framework snapshot internals |
|
|
30
|
+
|
|
31
|
+
Port-authored runtime and type tests are classified fail-closed in `audit/test-classifications.json`. The pristine→adapted case map and negative controls live in `audit/upstream-crosswalk.json` and `scripts/react-parity/react-textarea-autosize-controls.test.mjs`. Paired type evidence is recorded in `audit/type-parity.json` with assertion-group inventories.
|
|
32
|
+
|
|
33
|
+
The public callback remains named `onChange`, but Octane supplies a native input event rather than a React SyntheticEvent. The binding guarantees per-edit timing, target/currentTarget value during dispatch, bubbling, cancellation, and callback ordering. React-only event identity and fields are documented divergences.
|
package/package.json
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/textarea-autosize",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "react-textarea-autosize for Octane with matching measurement and lifecycle behavior.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Octane contributors"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
16
|
+
"directory": "packages/textarea-autosize"
|
|
17
|
+
},
|
|
18
|
+
"main": "src/index.tsrx",
|
|
19
|
+
"module": "src/index.tsrx",
|
|
20
|
+
"types": "src/index.tsrx",
|
|
21
|
+
"files": [
|
|
22
|
+
"src",
|
|
23
|
+
"README.md",
|
|
24
|
+
"UPSTREAM.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": {
|
|
31
|
+
"import": "./src/index.tsrx",
|
|
32
|
+
"default": "./src/index.tsrx"
|
|
33
|
+
},
|
|
34
|
+
"development": {
|
|
35
|
+
"edge-light": {
|
|
36
|
+
"module": "./src/index.tsrx",
|
|
37
|
+
"import": "./src/index.tsrx",
|
|
38
|
+
"default": "./src/index.tsrx"
|
|
39
|
+
},
|
|
40
|
+
"worker": {
|
|
41
|
+
"module": "./src/index.tsrx",
|
|
42
|
+
"import": "./src/index.tsrx",
|
|
43
|
+
"default": "./src/index.tsrx"
|
|
44
|
+
},
|
|
45
|
+
"workerd": {
|
|
46
|
+
"module": "./src/index.tsrx",
|
|
47
|
+
"import": "./src/index.tsrx",
|
|
48
|
+
"default": "./src/index.tsrx"
|
|
49
|
+
},
|
|
50
|
+
"browser": {
|
|
51
|
+
"module": "./src/index.tsrx",
|
|
52
|
+
"import": "./src/index.tsrx",
|
|
53
|
+
"default": "./src/index.tsrx"
|
|
54
|
+
},
|
|
55
|
+
"module": "./src/index.tsrx",
|
|
56
|
+
"import": "./src/index.tsrx",
|
|
57
|
+
"default": "./src/index.tsrx"
|
|
58
|
+
},
|
|
59
|
+
"edge-light": {
|
|
60
|
+
"module": "./src/index.tsrx",
|
|
61
|
+
"import": "./src/index.tsrx",
|
|
62
|
+
"default": "./src/index.tsrx"
|
|
63
|
+
},
|
|
64
|
+
"worker": {
|
|
65
|
+
"module": "./src/index.tsrx",
|
|
66
|
+
"import": "./src/index.tsrx",
|
|
67
|
+
"default": "./src/index.tsrx"
|
|
68
|
+
},
|
|
69
|
+
"workerd": {
|
|
70
|
+
"module": "./src/index.tsrx",
|
|
71
|
+
"import": "./src/index.tsrx",
|
|
72
|
+
"default": "./src/index.tsrx"
|
|
73
|
+
},
|
|
74
|
+
"browser": {
|
|
75
|
+
"module": "./src/index.tsrx",
|
|
76
|
+
"import": "./src/index.tsrx",
|
|
77
|
+
"default": "./src/index.tsrx"
|
|
78
|
+
},
|
|
79
|
+
"module": "./src/index.tsrx",
|
|
80
|
+
"import": "./src/index.tsrx",
|
|
81
|
+
"default": "./src/index.tsrx"
|
|
82
|
+
},
|
|
83
|
+
"./package.json": "./package.json"
|
|
84
|
+
},
|
|
85
|
+
"imports": {
|
|
86
|
+
"#is-development": {
|
|
87
|
+
"development": "./src/conditions/true.ts",
|
|
88
|
+
"default": "./src/conditions/false.ts"
|
|
89
|
+
},
|
|
90
|
+
"#is-browser": {
|
|
91
|
+
"edge-light": "./src/conditions/false.ts",
|
|
92
|
+
"workerd": "./src/conditions/false.ts",
|
|
93
|
+
"worker": "./src/conditions/false.ts",
|
|
94
|
+
"browser": "./src/conditions/true.ts",
|
|
95
|
+
"default": "./src/conditions/is-browser.ts"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"engines": {
|
|
99
|
+
"node": ">=22.22.2"
|
|
100
|
+
},
|
|
101
|
+
"peerDependencies": {
|
|
102
|
+
"octane": "0.1.35"
|
|
103
|
+
},
|
|
104
|
+
"devDependencies": {
|
|
105
|
+
"@babel/preset-env": "7.29.7",
|
|
106
|
+
"@babel/preset-react": "7.29.7",
|
|
107
|
+
"@babel/preset-typescript": "7.29.7",
|
|
108
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
109
|
+
"@testing-library/react": "16.3.2",
|
|
110
|
+
"@types/node": "^24.13.3",
|
|
111
|
+
"@types/react": "19.2.17",
|
|
112
|
+
"@types/react-dom": "19.2.3",
|
|
113
|
+
"@vitejs/plugin-react": "6.0.3",
|
|
114
|
+
"babel-jest": "30.4.1",
|
|
115
|
+
"jest": "30.4.2",
|
|
116
|
+
"jest-environment-jsdom": "30.4.1",
|
|
117
|
+
"playwright": "^1.61.1",
|
|
118
|
+
"react": "19.2.7",
|
|
119
|
+
"react-dom": "19.2.7",
|
|
120
|
+
"use-composed-ref": "1.3.0",
|
|
121
|
+
"use-latest": "1.2.1",
|
|
122
|
+
"typescript": "^5.9.3",
|
|
123
|
+
"vite": "^8.1.5",
|
|
124
|
+
"vitest": "^4.1.10",
|
|
125
|
+
"octane": "0.1.35"
|
|
126
|
+
},
|
|
127
|
+
"scripts": {
|
|
128
|
+
"test": "vitest run --root ../.. --project textarea-autosize --project textarea-autosize-ssr",
|
|
129
|
+
"test:browser": "vitest run --root ../.. --project textarea-autosize-browser",
|
|
130
|
+
"test:pristine": "CI=true jest --config jest.pristine.config.cjs --runInBand",
|
|
131
|
+
"test:types": "tsc -p audit/type-probes/tsconfig.pristine.json --noEmit && tsrx-tsc -p typetests/tsconfig.json --noEmit",
|
|
132
|
+
"verify:upstream": "node scripts/verify-upstream.mjs"
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import forceHiddenStyles from './forceHiddenStyles.js';
|
|
2
|
+
import type { SizingData } from './getSizingData.js';
|
|
3
|
+
|
|
4
|
+
export type CalculatedNodeHeights = number[];
|
|
5
|
+
let hiddenTextarea: HTMLTextAreaElement | null = null;
|
|
6
|
+
const getHeight = (node: HTMLElement, data: SizingData) =>
|
|
7
|
+
data.sizingStyle.boxSizing === 'border-box'
|
|
8
|
+
? node.scrollHeight + data.borderSize
|
|
9
|
+
: node.scrollHeight - data.paddingSize;
|
|
10
|
+
|
|
11
|
+
export default function calculateNodeHeight(
|
|
12
|
+
data: SizingData,
|
|
13
|
+
value: string,
|
|
14
|
+
minRows = 1,
|
|
15
|
+
maxRows = Infinity,
|
|
16
|
+
): CalculatedNodeHeights {
|
|
17
|
+
if (!hiddenTextarea) {
|
|
18
|
+
hiddenTextarea = document.createElement('textarea');
|
|
19
|
+
hiddenTextarea.setAttribute('tabindex', '-1');
|
|
20
|
+
hiddenTextarea.setAttribute('aria-hidden', 'true');
|
|
21
|
+
forceHiddenStyles(hiddenTextarea);
|
|
22
|
+
}
|
|
23
|
+
const measurementNode = hiddenTextarea;
|
|
24
|
+
if (measurementNode.parentNode === null) document.body.appendChild(measurementNode);
|
|
25
|
+
Object.assign(measurementNode.style, data.sizingStyle);
|
|
26
|
+
forceHiddenStyles(measurementNode);
|
|
27
|
+
measurementNode.value = value;
|
|
28
|
+
let height = getHeight(measurementNode, data);
|
|
29
|
+
measurementNode.value = value;
|
|
30
|
+
height = getHeight(measurementNode, data);
|
|
31
|
+
measurementNode.value = 'x';
|
|
32
|
+
const rowHeight = measurementNode.scrollHeight - data.paddingSize;
|
|
33
|
+
const boxDelta =
|
|
34
|
+
data.sizingStyle.boxSizing === 'border-box' ? data.paddingSize + data.borderSize : 0;
|
|
35
|
+
height = Math.max(rowHeight * minRows + boxDelta, height);
|
|
36
|
+
height = Math.min(rowHeight * maxRows + boxDelta, height);
|
|
37
|
+
return [height, rowHeight];
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default true;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const HIDDEN_TEXTAREA_STYLE = {
|
|
2
|
+
'min-height': '0',
|
|
3
|
+
'max-height': 'none',
|
|
4
|
+
height: '0',
|
|
5
|
+
visibility: 'hidden',
|
|
6
|
+
overflow: 'hidden',
|
|
7
|
+
position: 'absolute',
|
|
8
|
+
'z-index': '-1000',
|
|
9
|
+
top: '0',
|
|
10
|
+
right: '0',
|
|
11
|
+
display: 'block',
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export default function forceHiddenStyles(node: HTMLElement): void {
|
|
15
|
+
Object.keys(HIDDEN_TEXTAREA_STYLE).forEach((key) => {
|
|
16
|
+
node.style.setProperty(
|
|
17
|
+
key,
|
|
18
|
+
HIDDEN_TEXTAREA_STYLE[key as keyof typeof HIDDEN_TEXTAREA_STYLE],
|
|
19
|
+
'important',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import isBrowser from '#is-browser';
|
|
2
|
+
import { pick } from './utils.js';
|
|
3
|
+
|
|
4
|
+
const SIZING_STYLE = [
|
|
5
|
+
'borderBottomWidth',
|
|
6
|
+
'borderLeftWidth',
|
|
7
|
+
'borderRightWidth',
|
|
8
|
+
'borderTopWidth',
|
|
9
|
+
'boxSizing',
|
|
10
|
+
'fontFamily',
|
|
11
|
+
'fontSize',
|
|
12
|
+
'fontStyle',
|
|
13
|
+
'fontWeight',
|
|
14
|
+
'letterSpacing',
|
|
15
|
+
'lineHeight',
|
|
16
|
+
'paddingBottom',
|
|
17
|
+
'paddingLeft',
|
|
18
|
+
'paddingRight',
|
|
19
|
+
'paddingTop',
|
|
20
|
+
'tabSize',
|
|
21
|
+
'textIndent',
|
|
22
|
+
'textRendering',
|
|
23
|
+
'textTransform',
|
|
24
|
+
'width',
|
|
25
|
+
'wordBreak',
|
|
26
|
+
'wordSpacing',
|
|
27
|
+
'scrollbarGutter',
|
|
28
|
+
] as const;
|
|
29
|
+
type SizingProp = Extract<(typeof SIZING_STYLE)[number], keyof CSSStyleDeclaration>;
|
|
30
|
+
type SizingStyle = Pick<CSSStyleDeclaration, SizingProp>;
|
|
31
|
+
export type SizingData = { sizingStyle: SizingStyle; paddingSize: number; borderSize: number };
|
|
32
|
+
const isIE = isBrowser ? !!(document.documentElement as any).currentStyle : false;
|
|
33
|
+
|
|
34
|
+
export default function getSizingData(node: HTMLElement): SizingData | null {
|
|
35
|
+
const style = window.getComputedStyle(node);
|
|
36
|
+
if (style === null) return null;
|
|
37
|
+
const sizingStyle = pick(SIZING_STYLE as readonly SizingProp[], style);
|
|
38
|
+
if (sizingStyle.boxSizing === '') return null;
|
|
39
|
+
if (isIE && sizingStyle.boxSizing === 'border-box') {
|
|
40
|
+
sizingStyle.width = `${parseFloat(sizingStyle.width!) + parseFloat(sizingStyle.borderRightWidth!) + parseFloat(sizingStyle.borderLeftWidth!) + parseFloat(sizingStyle.paddingRight!) + parseFloat(sizingStyle.paddingLeft!)}px`;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
sizingStyle,
|
|
44
|
+
paddingSize: parseFloat(sizingStyle.paddingBottom!) + parseFloat(sizingStyle.paddingTop!),
|
|
45
|
+
borderSize:
|
|
46
|
+
parseFloat(sizingStyle.borderBottomWidth!) + parseFloat(sizingStyle.borderTopWidth!),
|
|
47
|
+
};
|
|
48
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** @jsxImportSource octane */
|
|
2
|
+
import { useEffectEvent, useLayoutEffect, useMemo } from 'octane';
|
|
3
|
+
|
|
4
|
+
type Ref<T> = ((value: T | null) => void) | { current: T | null } | null | undefined;
|
|
5
|
+
|
|
6
|
+
export function useComposedRef<T>(internal: { current: T | null }, external: Ref<T>) {
|
|
7
|
+
return useMemo(
|
|
8
|
+
() => (external == null ? internal : ([internal, external] as const)),
|
|
9
|
+
[internal, external],
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function useListener<K extends keyof WindowEventMap>(
|
|
14
|
+
target: Window,
|
|
15
|
+
type: K,
|
|
16
|
+
listener: (event: WindowEventMap[K]) => void,
|
|
17
|
+
): void;
|
|
18
|
+
function useListener<K extends keyof HTMLElementEventMap>(
|
|
19
|
+
target: HTMLElement,
|
|
20
|
+
type: K,
|
|
21
|
+
listener: (event: HTMLElementEventMap[K]) => void,
|
|
22
|
+
): void;
|
|
23
|
+
function useListener<K extends keyof FontFaceSetEventMap>(
|
|
24
|
+
target: FontFaceSet,
|
|
25
|
+
type: K,
|
|
26
|
+
listener: (event: FontFaceSetEventMap[K]) => void,
|
|
27
|
+
): void;
|
|
28
|
+
function useListener(
|
|
29
|
+
target: EventTarget | undefined,
|
|
30
|
+
type: string,
|
|
31
|
+
listener: (event: Event) => void,
|
|
32
|
+
): void {
|
|
33
|
+
const latestListener = useEffectEvent(listener);
|
|
34
|
+
useLayoutEffect(() => {
|
|
35
|
+
if (!target) return;
|
|
36
|
+
const handler = (event: Event) => latestListener(event);
|
|
37
|
+
target.addEventListener(type, handler);
|
|
38
|
+
return () => target.removeEventListener(type, handler);
|
|
39
|
+
}, []);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const useFormResetListener = (
|
|
43
|
+
ref: { current: HTMLTextAreaElement | null },
|
|
44
|
+
listener: (event: Event) => void,
|
|
45
|
+
) =>
|
|
46
|
+
useListener(document.body, 'reset', (event) => {
|
|
47
|
+
if (ref.current?.form === event.target) listener(event);
|
|
48
|
+
});
|
|
49
|
+
export const useWindowResizeListener = (listener: (event: UIEvent) => void) =>
|
|
50
|
+
useListener(window, 'resize', listener);
|
|
51
|
+
export const useFontsLoadedListener = (listener: (event: FontFaceSetLoadEvent) => void) =>
|
|
52
|
+
useListener(document.fonts, 'loadingdone', listener);
|
package/src/index.tsrx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import isBrowser from '#is-browser';
|
|
2
|
+
import isDevelopment from '#is-development';
|
|
3
|
+
import { useLayoutEffect, useRef } from 'octane';
|
|
4
|
+
import type { JSX } from 'octane/jsx-runtime';
|
|
5
|
+
import calculateNodeHeight from './calculateNodeHeight.js';
|
|
6
|
+
import getSizingData, { type SizingData } from './getSizingData.js';
|
|
7
|
+
import {
|
|
8
|
+
useComposedRef,
|
|
9
|
+
useFontsLoadedListener,
|
|
10
|
+
useFormResetListener,
|
|
11
|
+
useWindowResizeListener,
|
|
12
|
+
} from './hooks.js';
|
|
13
|
+
import { noop } from './utils.js';
|
|
14
|
+
|
|
15
|
+
type NativeTextareaProps = JSX.IntrinsicElements['textarea'];
|
|
16
|
+
type TextareaStyle =
|
|
17
|
+
Omit<Exclude<NonNullable<NativeTextareaProps['style']>, string>, 'maxHeight' | 'minHeight'> & {
|
|
18
|
+
height?: number;
|
|
19
|
+
};
|
|
20
|
+
type TextareaRef =
|
|
21
|
+
((value: HTMLTextAreaElement | null) => void) | { current: HTMLTextAreaElement | null } | null;
|
|
22
|
+
// OCTANE DIVERGENCE[native-input-event][differential:input]
|
|
23
|
+
type TextareaAutosizeChangeEvent =
|
|
24
|
+
InputEvent & {
|
|
25
|
+
currentTarget: HTMLTextAreaElement;
|
|
26
|
+
target: HTMLTextAreaElement;
|
|
27
|
+
};
|
|
28
|
+
export type TextareaHeightChangeMeta = { rowHeight: number };
|
|
29
|
+
export interface TextareaAutosizeProps extends Omit<
|
|
30
|
+
NativeTextareaProps,
|
|
31
|
+
'style' | 'onChange' | 'onChangeCapture' | 'ref'
|
|
32
|
+
> {
|
|
33
|
+
maxRows?: number;
|
|
34
|
+
minRows?: number;
|
|
35
|
+
onHeightChange?: (height: number, meta: TextareaHeightChangeMeta) => void;
|
|
36
|
+
cacheMeasurements?: boolean;
|
|
37
|
+
style?: TextareaStyle;
|
|
38
|
+
onChange?: (event: TextareaAutosizeChangeEvent) => void;
|
|
39
|
+
onChangeCapture?: (event: TextareaAutosizeChangeEvent) => void;
|
|
40
|
+
ref?: TextareaRef;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function TextareaAutosize(
|
|
44
|
+
{
|
|
45
|
+
cacheMeasurements,
|
|
46
|
+
maxRows,
|
|
47
|
+
minRows,
|
|
48
|
+
onChange = noop,
|
|
49
|
+
onChangeCapture = noop,
|
|
50
|
+
onHeightChange = noop,
|
|
51
|
+
onInput,
|
|
52
|
+
onInputCapture,
|
|
53
|
+
ref: userRef,
|
|
54
|
+
...props
|
|
55
|
+
}: TextareaAutosizeProps = {},
|
|
56
|
+
) @{
|
|
57
|
+
if (isDevelopment && props.style) {
|
|
58
|
+
if (
|
|
59
|
+
'maxHeight' in props.style
|
|
60
|
+
) throw new Error('Using `style.maxHeight` for <TextareaAutosize/> is not supported. Please use `maxRows`.');
|
|
61
|
+
if (
|
|
62
|
+
'minHeight' in props.style
|
|
63
|
+
) throw new Error('Using `style.minHeight` for <TextareaAutosize/> is not supported. Please use `minRows`.');
|
|
64
|
+
}
|
|
65
|
+
const isControlled = props.value !== undefined;
|
|
66
|
+
const libRef = useRef<HTMLTextAreaElement | null>(null);
|
|
67
|
+
const ref = useComposedRef(libRef, userRef);
|
|
68
|
+
const heightRef = useRef(0);
|
|
69
|
+
const measurementsCacheRef = useRef<SizingData | undefined>(undefined);
|
|
70
|
+
const resizeTextarea = () => {
|
|
71
|
+
const node = libRef.current;
|
|
72
|
+
if (!node) return;
|
|
73
|
+
const sizingData =
|
|
74
|
+
cacheMeasurements && measurementsCacheRef.current
|
|
75
|
+
? measurementsCacheRef.current
|
|
76
|
+
: getSizingData(node);
|
|
77
|
+
if (!sizingData) return;
|
|
78
|
+
measurementsCacheRef.current = sizingData;
|
|
79
|
+
const [height, rowHeight] = calculateNodeHeight(
|
|
80
|
+
sizingData,
|
|
81
|
+
node.value || node.placeholder || 'x',
|
|
82
|
+
minRows,
|
|
83
|
+
maxRows,
|
|
84
|
+
);
|
|
85
|
+
if (heightRef.current !== height) {
|
|
86
|
+
heightRef.current = height;
|
|
87
|
+
node.style.setProperty('height', `${height}px`, 'important');
|
|
88
|
+
onHeightChange(height, { rowHeight });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const handleInput: NonNullable<NativeTextareaProps['onInput']> = (event) => {
|
|
92
|
+
onInput?.(event);
|
|
93
|
+
onChangeCapture(event as TextareaAutosizeChangeEvent);
|
|
94
|
+
if (!isControlled) resizeTextarea();
|
|
95
|
+
onChange(event as TextareaAutosizeChangeEvent);
|
|
96
|
+
};
|
|
97
|
+
if (isBrowser) {
|
|
98
|
+
useLayoutEffect(resizeTextarea, null);
|
|
99
|
+
useFormResetListener(libRef, () => {
|
|
100
|
+
const node = libRef.current;
|
|
101
|
+
if (isControlled || !node) return;
|
|
102
|
+
const currentValue = node.value;
|
|
103
|
+
requestAnimationFrame(() => {
|
|
104
|
+
if (libRef.current && currentValue !== libRef.current.value) resizeTextarea();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
useWindowResizeListener(resizeTextarea);
|
|
108
|
+
useFontsLoadedListener(resizeTextarea);
|
|
109
|
+
}
|
|
110
|
+
<textarea {...props} onInput={handleInput} onInputCapture={onInputCapture} ref={ref} />
|
|
111
|
+
}
|
package/src/utils.ts
ADDED