testomatio-editor-blocks 0.3.0 → 0.4.0

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.
@@ -1,8 +1,131 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createReactBlockSpec } from "@blocknote/react";
3
- import { useCallback } from "react";
3
+ import OverType from "overtype";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
5
  import { StepField } from "./stepField";
5
6
  import { useSnippetAutocomplete } from "../snippetAutocomplete";
7
+ import { useStepImageUpload } from "../stepImageUpload";
8
+ function SnippetDataField({ label, value, placeholder, onChange, onFieldFocus, fieldName, enableImageUpload = false, }) {
9
+ const containerRef = useRef(null);
10
+ const instanceRef = useRef(null);
11
+ const uploadImage = useStepImageUpload();
12
+ const [isFocused, setIsFocused] = useState(false);
13
+ const onChangeRef = useRef(onChange);
14
+ const initialValueRef = useRef(value);
15
+ useEffect(() => {
16
+ onChangeRef.current = onChange;
17
+ }, [onChange]);
18
+ const insertImageMarkdown = useCallback((url) => {
19
+ var _a, _b, _c;
20
+ const instance = instanceRef.current;
21
+ const textarea = instance === null || instance === void 0 ? void 0 : instance.textarea;
22
+ if (!instance || !textarea) {
23
+ return;
24
+ }
25
+ const currentValue = instance.getValue();
26
+ const selectionStart = (_a = textarea.selectionStart) !== null && _a !== void 0 ? _a : currentValue.length;
27
+ const selectionEnd = (_b = textarea.selectionEnd) !== null && _b !== void 0 ? _b : currentValue.length;
28
+ const before = currentValue.slice(0, selectionStart);
29
+ const after = currentValue.slice(selectionEnd);
30
+ const needsNewlineBefore = before.length > 0 && !before.endsWith("\n");
31
+ const needsNewlineAfter = after.length > 0 && !after.startsWith("\n");
32
+ const markdown = `${needsNewlineBefore ? "\n" : ""}![](${url})${needsNewlineAfter ? "\n" : ""}`;
33
+ const nextValue = `${before}${markdown}${after}`;
34
+ instance.setValue(nextValue);
35
+ (_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, nextValue);
36
+ requestAnimationFrame(() => {
37
+ const cursor = selectionStart + markdown.length;
38
+ textarea.selectionStart = cursor;
39
+ textarea.selectionEnd = cursor;
40
+ textarea.focus();
41
+ });
42
+ }, []);
43
+ useEffect(() => {
44
+ const container = containerRef.current;
45
+ if (!container) {
46
+ return;
47
+ }
48
+ const [instance] = OverType.init(container, {
49
+ value: initialValueRef.current,
50
+ placeholder,
51
+ autoResize: true,
52
+ minHeight: "5rem",
53
+ toolbar: false,
54
+ onChange: (nextValue) => {
55
+ var _a;
56
+ (_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, nextValue);
57
+ },
58
+ });
59
+ instanceRef.current = instance;
60
+ const textarea = instance.textarea;
61
+ if (fieldName) {
62
+ textarea.dataset.stepField = fieldName;
63
+ }
64
+ const handleFocus = () => {
65
+ setIsFocused(true);
66
+ onFieldFocus === null || onFieldFocus === void 0 ? void 0 : onFieldFocus();
67
+ };
68
+ const handleBlur = () => setIsFocused(false);
69
+ textarea.addEventListener("focus", handleFocus);
70
+ textarea.addEventListener("blur", handleBlur);
71
+ return () => {
72
+ textarea.removeEventListener("focus", handleFocus);
73
+ textarea.removeEventListener("blur", handleBlur);
74
+ instance.destroy();
75
+ instanceRef.current = null;
76
+ };
77
+ }, [fieldName, onFieldFocus, placeholder]);
78
+ useEffect(() => {
79
+ const instance = instanceRef.current;
80
+ if (!instance) {
81
+ return;
82
+ }
83
+ if (instance.getValue() !== value) {
84
+ instance.setValue(value);
85
+ }
86
+ }, [value]);
87
+ useEffect(() => {
88
+ var _a;
89
+ if (!enableImageUpload || !uploadImage) {
90
+ return;
91
+ }
92
+ const textarea = (_a = instanceRef.current) === null || _a === void 0 ? void 0 : _a.textarea;
93
+ if (!textarea) {
94
+ return;
95
+ }
96
+ const handlePaste = async (event) => {
97
+ var _a, _b;
98
+ const items = Array.from((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : []);
99
+ const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
100
+ const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
101
+ if (!file) {
102
+ return;
103
+ }
104
+ event.preventDefault();
105
+ try {
106
+ const response = await uploadImage(file);
107
+ if (response === null || response === void 0 ? void 0 : response.url) {
108
+ insertImageMarkdown(response.url);
109
+ }
110
+ }
111
+ catch (error) {
112
+ console.error("Failed to upload pasted image", error);
113
+ }
114
+ };
115
+ textarea.addEventListener("paste", handlePaste);
116
+ return () => {
117
+ textarea.removeEventListener("paste", handlePaste);
118
+ };
119
+ }, [enableImageUpload, insertImageMarkdown, uploadImage]);
120
+ const editorClassName = useMemo(() => [
121
+ "bn-step-editor",
122
+ "bn-step-editor--multiline",
123
+ isFocused ? "bn-step-editor--focused" : "",
124
+ ]
125
+ .filter(Boolean)
126
+ .join(" "), [isFocused]);
127
+ return (_jsxs("div", { className: "bn-step-field", children: [_jsx("div", { className: "bn-step-field__top", children: _jsx("span", { className: "bn-step-field__label", children: label }) }), _jsx("div", { ref: containerRef, className: editorClassName, "data-step-field": fieldName })] }));
128
+ }
6
129
  export const snippetBlock = createReactBlockSpec({
7
130
  type: "snippet",
8
131
  content: "none",
@@ -24,8 +147,10 @@ export const snippetBlock = createReactBlockSpec({
24
147
  render: ({ block, editor }) => {
25
148
  const snippetTitle = block.props.snippetTitle || "";
26
149
  const snippetData = block.props.snippetData || "";
150
+ const snippetId = block.props.snippetId || "";
27
151
  const snippetSuggestions = useSnippetAutocomplete();
28
152
  const hasSnippets = snippetSuggestions.length > 0;
153
+ const isSnippetSelected = snippetId.length > 0;
29
154
  const handleSnippetChange = useCallback((nextTitle) => {
30
155
  if (nextTitle === snippetTitle) {
31
156
  return;
@@ -67,6 +192,6 @@ export const snippetBlock = createReactBlockSpec({
67
192
  if (!hasSnippets) {
68
193
  return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: _jsx("p", { className: "bn-snippet__empty", children: "No snippets in this project." }) }));
69
194
  }
70
- return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: [_jsx(StepField, { label: "Snippet Title", value: snippetTitle, onChange: handleSnippetChange, autoFocus: snippetTitle.length === 0, enableAutocomplete: true, suggestionFilter: (suggestion) => suggestion.isSnippet === true, suggestionsOverride: snippetSuggestions, onSuggestionSelect: handleSnippetSelect, fieldName: "snippet-title", showSuggestionsOnFocus: true, enableImageUpload: false, onFieldFocus: handleFieldFocus }), _jsx(StepField, { label: "Snippet Data", value: snippetData, onChange: handleSnippetDataChange, multiline: true, fieldName: "snippet-data", enableImageUpload: true, onFieldFocus: handleFieldFocus })] }));
195
+ return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: !isSnippetSelected ? (_jsx(StepField, { label: "Snippet Title", value: snippetTitle, onChange: handleSnippetChange, autoFocus: snippetTitle.length === 0, enableAutocomplete: true, suggestionFilter: (suggestion) => suggestion.isSnippet === true, suggestionsOverride: snippetSuggestions, onSuggestionSelect: handleSnippetSelect, fieldName: "snippet-title", showSuggestionsOnFocus: true, enableImageUpload: false, onFieldFocus: handleFieldFocus, readOnly: false })) : (_jsx(SnippetDataField, { label: `Snippet: ${snippetTitle}`, value: snippetData, onChange: handleSnippetDataChange, fieldName: "snippet-data", enableImageUpload: true, onFieldFocus: handleFieldFocus, placeholder: "Snippet data will appear here..." })) }));
71
196
  },
72
197
  });
@@ -1,8 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createReactBlockSpec } from "@blocknote/react";
3
- import { useCallback, useEffect, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import { StepField } from "./stepField";
5
5
  import { useStepImageUpload } from "../stepImageUpload";
6
+ const EXPECTED_COLLAPSED_KEY = "bn-expected-collapsed";
7
+ const readExpectedCollapsedPreference = () => {
8
+ if (typeof window === "undefined") {
9
+ return false;
10
+ }
11
+ try {
12
+ return window.localStorage.getItem(EXPECTED_COLLAPSED_KEY) === "true";
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ };
18
+ const writeExpectedCollapsedPreference = (collapsed) => {
19
+ if (typeof window === "undefined") {
20
+ return;
21
+ }
22
+ try {
23
+ window.localStorage.setItem(EXPECTED_COLLAPSED_KEY, collapsed ? "true" : "false");
24
+ }
25
+ catch {
26
+ //
27
+ }
28
+ };
6
29
  export const stepBlock = createReactBlockSpec({
7
30
  type: "testStep",
8
31
  content: "none",
@@ -22,22 +45,25 @@ export const stepBlock = createReactBlockSpec({
22
45
  const stepTitle = block.props.stepTitle || "";
23
46
  const stepData = block.props.stepData || "";
24
47
  const expectedResult = block.props.expectedResult || "";
25
- const showExpectedField = stepTitle.trim().length > 0 || stepData.trim().length > 0 || expectedResult.trim().length > 0;
26
- const [isDataVisible, setIsDataVisible] = useState(() => stepData.trim().length > 0);
48
+ const expectedHasContent = expectedResult.trim().length > 0;
49
+ const storedExpectedCollapsed = useMemo(() => readExpectedCollapsedPreference(), []);
50
+ const dataHasContent = stepData.trim().length > 0;
51
+ const [isExpectedVisible, setIsExpectedVisible] = useState(expectedHasContent ? true : !storedExpectedCollapsed);
52
+ const [isDataVisible, setIsDataVisible] = useState(dataHasContent);
27
53
  const [shouldFocusDataField, setShouldFocusDataField] = useState(false);
28
54
  const uploadImage = useStepImageUpload();
55
+ // Calculate step number based on position in document
56
+ const stepNumber = useMemo(() => {
57
+ const allBlocks = editor.document;
58
+ const stepBlocks = allBlocks.filter((b) => b.type === "testStep");
59
+ const index = stepBlocks.findIndex((b) => b.id === block.id);
60
+ return index >= 0 ? index + 1 : 1;
61
+ }, [editor.document, block.id]);
29
62
  useEffect(() => {
30
- if (stepData.trim().length > 0 && !isDataVisible) {
63
+ if (dataHasContent && !isDataVisible) {
31
64
  setIsDataVisible(true);
32
65
  }
33
- }, [isDataVisible, stepData]);
34
- useEffect(() => {
35
- if (shouldFocusDataField && isDataVisible) {
36
- const timer = setTimeout(() => setShouldFocusDataField(false), 0);
37
- return () => clearTimeout(timer);
38
- }
39
- return undefined;
40
- }, [isDataVisible, shouldFocusDataField]);
66
+ }, [dataHasContent, isDataVisible]);
41
67
  const handleStepTitleChange = useCallback((next) => {
42
68
  if (next === stepTitle) {
43
69
  return;
@@ -58,10 +84,13 @@ export const stepBlock = createReactBlockSpec({
58
84
  },
59
85
  });
60
86
  }, [editor, block.id, stepData]);
61
- const handleShowDataField = useCallback(() => {
87
+ const handleShowData = useCallback(() => {
62
88
  setIsDataVisible(true);
63
89
  setShouldFocusDataField(true);
64
90
  }, []);
91
+ const handleHideData = useCallback(() => {
92
+ setIsDataVisible(false);
93
+ }, []);
65
94
  const handleExpectedChange = useCallback((next) => {
66
95
  if (next === expectedResult) {
67
96
  return;
@@ -88,7 +117,25 @@ export const stepBlock = createReactBlockSpec({
88
117
  const handleFieldFocus = useCallback(() => {
89
118
  editor.setSelection(block.id, block.id);
90
119
  }, [editor, block.id]);
91
- return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: "Step Title", value: stepTitle, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, rightAction: !isDataVisible ? (_jsx("button", { type: "button", className: "bn-teststep__toggle", onClick: handleShowDataField, "aria-expanded": "false", tabIndex: -1, children: "+ Step Data" })) : null, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
120
+ const [dataFocusSignal] = useState(0);
121
+ const [expectedFocusSignal, setExpectedFocusSignal] = useState(0);
122
+ const handleShowExpected = useCallback(() => {
123
+ setIsExpectedVisible(true);
124
+ setExpectedFocusSignal((value) => value + 1);
125
+ writeExpectedCollapsedPreference(false);
126
+ }, []);
127
+ const handleHideExpected = useCallback(() => {
128
+ setIsExpectedVisible(false);
129
+ writeExpectedCollapsedPreference(true);
130
+ }, []);
131
+ useEffect(() => {
132
+ if (expectedHasContent && !isExpectedVisible) {
133
+ setIsExpectedVisible(true);
134
+ }
135
+ }, [expectedHasContent, isExpectedVisible]);
136
+ const canToggleData = !dataHasContent;
137
+ const canToggleExpected = !expectedHasContent;
138
+ return (_jsxs("div", { className: "bn-teststep", "data-block-id": block.id, children: [_jsx(StepField, { label: `Step ${stepNumber}`, value: stepTitle, onChange: handleStepTitleChange, autoFocus: stepTitle.length === 0, enableAutocomplete: true, fieldName: "title", suggestionFilter: (suggestion) => suggestion.isSnippet !== true, onFieldFocus: handleFieldFocus, enableImageUpload: false, showFormattingButtons: true, onImageFile: async (file) => {
92
139
  if (!uploadImage) {
93
140
  return;
94
141
  }
@@ -108,6 +155,26 @@ export const stepBlock = createReactBlockSpec({
108
155
  catch (error) {
109
156
  console.error("Failed to upload image to Step Data", error);
110
157
  }
111
- } }), isDataVisible && (_jsx(StepField, { label: "Step Data", value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), showExpectedField && (_jsx(StepField, { label: "Expected Result", value: expectedResult, onChange: handleExpectedChange, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })), _jsx("button", { type: "button", className: "bn-step-add", onClick: handleInsertNextStep, children: "+ Step" })] }));
158
+ } }), isDataVisible ? (_jsx(StepField, { label: "Step Data", labelToggle: canToggleData
159
+ ? {
160
+ onClick: handleHideData,
161
+ expanded: true,
162
+ }
163
+ : undefined, value: stepData, onChange: handleStepDataChange, autoFocus: shouldFocusDataField, focusSignal: dataFocusSignal, multiline: true, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : (_jsx("div", { className: "bn-step-field bn-step-field--collapsed", children: _jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: handleShowData, onKeyDown: (event) => {
164
+ if (event.key === "Enter" || event.key === " ") {
165
+ event.preventDefault();
166
+ handleShowData();
167
+ }
168
+ }, "aria-expanded": "false", children: "Step Data" }) })), isExpectedVisible ? (_jsx(StepField, { label: "Expected Result", labelToggle: canToggleExpected
169
+ ? {
170
+ onClick: handleHideExpected,
171
+ expanded: true,
172
+ }
173
+ : undefined, value: expectedResult, onChange: handleExpectedChange, multiline: true, focusSignal: expectedFocusSignal, enableImageUpload: true, showFormattingButtons: true, showImageButton: true, onFieldFocus: handleFieldFocus })) : (_jsx("div", { className: "bn-step-field bn-step-field--collapsed", children: _jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: handleShowExpected, onKeyDown: (event) => {
174
+ if (event.key === "Enter" || event.key === " ") {
175
+ event.preventDefault();
176
+ handleShowExpected();
177
+ }
178
+ }, "aria-expanded": "false", children: "Expected Result" }) })), _jsx("button", { type: "button", className: "bn-step-add", onClick: handleInsertNextStep, children: "+ Step" })] }));
112
179
  },
113
180
  });
@@ -4,9 +4,14 @@ import { type SnippetSuggestion } from "../snippetAutocomplete";
4
4
  type Suggestion = StepSuggestion | SnippetSuggestion;
5
5
  type StepFieldProps = {
6
6
  label: string;
7
+ labelToggle?: {
8
+ onClick: () => void;
9
+ expanded: boolean;
10
+ };
7
11
  value: string;
8
12
  onChange: (nextValue: string) => void;
9
13
  autoFocus?: boolean;
14
+ focusSignal?: number;
10
15
  multiline?: boolean;
11
16
  enableAutocomplete?: boolean;
12
17
  fieldName?: string;
@@ -22,5 +27,5 @@ type StepFieldProps = {
22
27
  showImageButton?: boolean;
23
28
  onFieldFocus?: () => void;
24
29
  };
25
- export declare function StepField({ label, value, onChange, autoFocus, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
30
+ export declare function StepField({ label, labelToggle, value, onChange, autoFocus, focusSignal, multiline, enableAutocomplete, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly, showSuggestionsOnFocus, enableImageUpload, onImageFile, rightAction, showFormattingButtons, showImageButton, onFieldFocus, }: StepFieldProps): import("react/jsx-runtime").JSX.Element;
26
31
  export {};
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { useStepAutocomplete } from "../stepAutocomplete";
5
5
  import { useStepImageUpload } from "../stepImageUpload";
6
6
  import { escapeMarkdownText, normalizePlainText } from "./markdown";
7
+ import { useAutoResize } from "./useAutoResize";
7
8
  const READ_ONLY_ALLOWED_KEYS = new Set([
8
9
  "ArrowDown",
9
10
  "ArrowUp",
@@ -30,7 +31,7 @@ function markdownToPlainText(markdown) {
30
31
  return markdown.replace(/!\[[^\]]*]\([^)]+\)/g, "").replace(/\[[^\]]*]\([^)]+\)/g, "").replace(/[*_`~]/g, "").replace(/\s+/g, " ").trim();
31
32
  }
32
33
  }
33
- export function StepField({ label, value, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
34
+ export function StepField({ label, labelToggle, value, onChange, autoFocus, focusSignal, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, onFieldFocus, }) {
34
35
  const stepSuggestions = useStepAutocomplete();
35
36
  const suggestions = suggestionsOverride !== null && suggestionsOverride !== void 0 ? suggestionsOverride : stepSuggestions;
36
37
  const uploadImage = useStepImageUpload();
@@ -86,6 +87,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
86
87
  textareaNode.focus();
87
88
  }
88
89
  }, [textareaNode]);
90
+ useEffect(() => {
91
+ if (!textareaNode || !focusSignal) {
92
+ return;
93
+ }
94
+ textareaNode.focus();
95
+ }, [focusSignal, textareaNode]);
89
96
  useEffect(() => {
90
97
  const instance = editorInstanceRef.current;
91
98
  if (!instance) {
@@ -120,6 +127,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
120
127
  }
121
128
  textareaNode.readOnly = readOnly;
122
129
  }, [readOnly, textareaNode]);
130
+ useAutoResize({
131
+ textarea: textareaNode,
132
+ multiline,
133
+ minRows: 3,
134
+ maxRows: 16,
135
+ });
123
136
  useEffect(() => {
124
137
  if (!textareaNode) {
125
138
  return;
@@ -409,7 +422,12 @@ export function StepField({ label, value, onChange, autoFocus, multiline = false
409
422
  ]
410
423
  .filter(Boolean)
411
424
  .join(" ");
412
- return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("span", { className: "bn-step-field__label", children: [label, enableAutocomplete && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
425
+ return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("div", { className: "bn-step-field__label-row", children: [labelToggle ? (_jsx("span", { className: "bn-step-field__label bn-step-field__label--toggle", role: "button", tabIndex: -1, onClick: labelToggle.onClick, onKeyDown: (event) => {
426
+ if (event.key === "Enter" || event.key === " ") {
427
+ event.preventDefault();
428
+ labelToggle.onClick();
429
+ }
430
+ }, "aria-expanded": labelToggle.expanded, children: label })) : (_jsx("span", { className: "bn-step-field__label", children: label })), enableAutocomplete && (_jsx("button", { type: "button", className: "bn-step-suggestions-toggle", onMouseDown: (event) => {
413
431
  event.preventDefault();
414
432
  setShowAllSuggestions(true);
415
433
  textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus();
@@ -0,0 +1,8 @@
1
+ type Options = {
2
+ textarea: HTMLTextAreaElement | null;
3
+ multiline?: boolean;
4
+ minRows?: number;
5
+ maxRows?: number;
6
+ };
7
+ export declare function useAutoResize({ textarea, multiline, minRows, maxRows }: Options): void;
8
+ export {};
@@ -0,0 +1,31 @@
1
+ import { useEffect, useRef } from "react";
2
+ export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRows = 12 }) {
3
+ const frameRef = useRef(0);
4
+ useEffect(() => {
5
+ if (!textarea || !multiline) {
6
+ return;
7
+ }
8
+ const resize = () => {
9
+ textarea.style.height = "auto";
10
+ const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
11
+ const minHeight = lineHeight * minRows;
12
+ const maxHeight = lineHeight * maxRows;
13
+ textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight)}px`;
14
+ };
15
+ const observer = new MutationObserver(resize);
16
+ observer.observe(textarea, { childList: true, characterData: true, subtree: true });
17
+ const handleInput = () => {
18
+ var _a;
19
+ cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
20
+ frameRef.current = requestAnimationFrame(resize);
21
+ };
22
+ textarea.addEventListener("input", handleInput);
23
+ resize();
24
+ return () => {
25
+ var _a;
26
+ observer.disconnect();
27
+ textarea.removeEventListener("input", handleInput);
28
+ cancelAnimationFrame((_a = frameRef.current) !== null && _a !== void 0 ? _a : 0);
29
+ };
30
+ }, [textarea, multiline, minRows, maxRows]);
31
+ }