@capyx/components-library 0.0.17 → 0.0.18
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/README.md
CHANGED
|
@@ -176,11 +176,11 @@ function MyForm() {
|
|
|
176
176
|
|
|
177
177
|
By default the editor has no maximum height. In a constrained context — such as a Bootstrap `Modal` — you want the editor to grow until the modal fills the viewport, then scroll inside the editor instead of overflowing the page.
|
|
178
178
|
|
|
179
|
-
Three things are required:
|
|
179
|
+
Use the `constrainHeight` prop together with a `style` that sets the height. Three things are required:
|
|
180
180
|
|
|
181
181
|
1. **`<Modal scrollable>`** — Bootstrap caps the modal body at the available viewport height.
|
|
182
182
|
2. **`d-flex flex-column overflow-hidden` on `<Modal.Body>`** — turns the body into a flex column that constrains its children to the available height.
|
|
183
|
-
3. **`style={{ flex: 1, minHeight: 0 }}` on `<RichTextInput>`** — `flex: 1` fills the remaining space; `minHeight: 0` is required in a flex context so the wrapper can shrink below its natural size
|
|
183
|
+
3. **`constrainHeight` + `style={{ flex: 1, minHeight: 0 }}` on `<RichTextInput>`** — `constrainHeight` activates the CSS flex chain through Quill's internal DOM nodes; `flex: 1` fills the remaining space; `minHeight: 0` is required in a flex context so the wrapper can shrink below its natural size.
|
|
184
184
|
|
|
185
185
|
```tsx
|
|
186
186
|
import { RichTextInput } from '@your-package/components-library';
|
|
@@ -200,6 +200,7 @@ function MyModal({ show, onHide }) {
|
|
|
200
200
|
<RichTextInput
|
|
201
201
|
value={content}
|
|
202
202
|
onChange={setContent}
|
|
203
|
+
constrainHeight
|
|
203
204
|
{/* flex:1 fills remaining space; minHeight:0 enables overflow */}
|
|
204
205
|
style={{ flex: 1, minHeight: 0 }}
|
|
205
206
|
/>
|
|
@@ -226,6 +227,59 @@ function MyModal({ show, onHide }) {
|
|
|
226
227
|
| `formats` | `FormatType[]` | `['header','bold','italic','underline','list']` | Allowed Quill formats |
|
|
227
228
|
| `style` | `CSSProperties` | — | Inline styles on the outer wrapper `div`. Use to constrain height in a flex context |
|
|
228
229
|
| `wrapperClassName` | `string` | — | Extra CSS class names on the outer wrapper `div` |
|
|
230
|
+
| `constrainHeight` | `boolean` | `false` | Activates internal flex chain so the editor scrolls inside itself. Requires a height to be set via `style` (e.g. `style={{ flex: 1, minHeight: 0 }}` or `style={{ height: '40vh' }}`) |
|
|
231
|
+
|
|
232
|
+
### TextAreaInput
|
|
233
|
+
|
|
234
|
+
`TextAreaInput` is a multiline text input that **auto-grows with its content** using a row-counting approach. There are no inline `style` height overrides, so consumers can freely customise appearance with plain CSS.
|
|
235
|
+
|
|
236
|
+
#### Basic usage
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { TextAreaInput } from '@capyx/components-library';
|
|
240
|
+
import { FormProvider, useForm } from 'react-hook-form';
|
|
241
|
+
|
|
242
|
+
function MyForm() {
|
|
243
|
+
const methods = useForm();
|
|
244
|
+
return (
|
|
245
|
+
<FormProvider {...methods}>
|
|
246
|
+
<TextAreaInput name="notes" label="Notes" placeholder="Enter notes…" />
|
|
247
|
+
</FormProvider>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Controlling row height
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
{/* Starts at 5 rows, grows without limit */}
|
|
256
|
+
<TextAreaInput name="bio" label="Biography" minRows={5} />
|
|
257
|
+
|
|
258
|
+
{/* Starts at 2 rows, scrolls internally after 8 rows */}
|
|
259
|
+
<TextAreaInput name="summary" label="Summary" maxRows={8} />
|
|
260
|
+
|
|
261
|
+
{/* Fixed band: always between 3 and 6 rows */}
|
|
262
|
+
<TextAreaInput name="notes" label="Notes" minRows={3} maxRows={6} />
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
#### Props
|
|
266
|
+
|
|
267
|
+
| Prop | Type | Default | Description |
|
|
268
|
+
|---|---|---|---|
|
|
269
|
+
| `name` | `string` | — | Field name for form registration |
|
|
270
|
+
| `label` | `string` | — | Label text |
|
|
271
|
+
| `required` | `boolean` | `false` | Marks the field as required |
|
|
272
|
+
| `maxLength` | `number` | — | Maximum number of characters allowed |
|
|
273
|
+
| `placeholder` | `string` | — | Placeholder text |
|
|
274
|
+
| `value` | `string` | — | Controlled value (standalone mode) |
|
|
275
|
+
| `onChange` | `(value: string) => void` | — | Change callback |
|
|
276
|
+
| `disabled` | `boolean` | `false` | Disables the field |
|
|
277
|
+
| `isReadOnly` | `boolean` | `false` | Makes the field read-only |
|
|
278
|
+
| `isPlainText` | `boolean` | `false` | Renders as plain text (no border) |
|
|
279
|
+
| `controlSize` | `'sm' \| 'lg'` | — | Bootstrap size variant |
|
|
280
|
+
| `debounceMs` | `number` | — | Debounce delay in milliseconds for `onChange` |
|
|
281
|
+
| `minRows` | `number` | `2` | Minimum number of visible rows |
|
|
282
|
+
| `maxRows` | `number` | — | Maximum rows before the textarea scrolls internally. Unset = grows without limit |
|
|
229
283
|
|
|
230
284
|
### Using Addons
|
|
231
285
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type FC } from 'react';
|
|
2
|
+
import './TextAreaInput.css';
|
|
2
3
|
/**
|
|
3
4
|
* Props for the TextAreaInput component
|
|
4
5
|
*/
|
|
@@ -27,6 +28,16 @@ export type TextAreaInputProps = {
|
|
|
27
28
|
isPlainText?: boolean;
|
|
28
29
|
/** Debounce delay in milliseconds for value changes */
|
|
29
30
|
debounceMs?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Minimum number of visible rows.
|
|
33
|
+
* @default 2
|
|
34
|
+
*/
|
|
35
|
+
minRows?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Maximum number of visible rows before the textarea scrolls internally.
|
|
38
|
+
* When unset the textarea grows without any row cap.
|
|
39
|
+
*/
|
|
40
|
+
maxRows?: number;
|
|
30
41
|
};
|
|
31
42
|
/**
|
|
32
43
|
* A flexible textarea input component with automatic height adjustment,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../lib/components/TextAreaInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,EAAE,EAKP,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"TextAreaInput.d.ts","sourceRoot":"","sources":["../../lib/components/TextAreaInput.tsx"],"names":[],"mappings":"AACA,OAAO,EAEN,KAAK,EAAE,EAKP,MAAM,OAAO,CAAC;AAGf,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAChC,qCAAqC;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,sCAAsC;IACtC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CAyIhD,CAAC"}
|
|
@@ -3,7 +3,8 @@ import debounce from 'lodash.debounce';
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
4
|
import { Form } from 'react-bootstrap';
|
|
5
5
|
import { Controller, useFormContext } from 'react-hook-form';
|
|
6
|
-
|
|
6
|
+
import './TextAreaInput.css';
|
|
7
|
+
const MIN_ROWS = 2;
|
|
7
8
|
/**
|
|
8
9
|
* A flexible textarea input component with automatic height adjustment,
|
|
9
10
|
* react-hook-form integration, and optional debouncing.
|
|
@@ -43,7 +44,8 @@ const MIN_TEXTAREA_HEIGHT = 32;
|
|
|
43
44
|
* placeholder="Add your comment..."
|
|
44
45
|
* />
|
|
45
46
|
*/
|
|
46
|
-
export const TextAreaInput = ({ name, label, required = false, maxLength, controlSize, placeholder, value, onChange, disabled = false, isReadOnly = false, isPlainText = false, debounceMs, }) => {
|
|
47
|
+
export const TextAreaInput = ({ name, label, required = false, maxLength, controlSize, placeholder, value, onChange, disabled = false, isReadOnly = false, isPlainText = false, debounceMs, minRows, maxRows, }) => {
|
|
48
|
+
const resolvedMinRows = minRows ?? MIN_ROWS;
|
|
47
49
|
const formContext = useFormContext();
|
|
48
50
|
// Create ref for debounced onChange to clean up on unmount
|
|
49
51
|
const debouncedOnChangeRef = useRef(null);
|
|
@@ -78,17 +80,27 @@ export const TextAreaInput = ({ name, label, required = false, maxLength, contro
|
|
|
78
80
|
const errorMessage = getFieldError(name);
|
|
79
81
|
const isInvalid = !!errorMessage;
|
|
80
82
|
const textareaRef = useRef(null);
|
|
81
|
-
const [
|
|
83
|
+
const [rowCount, setRowCount] = useState(resolvedMinRows);
|
|
82
84
|
const _handleTextAreaChange = (event) => {
|
|
83
|
-
setTextAreaValue(event.target.value);
|
|
84
85
|
handleChange(event.target.value);
|
|
85
86
|
};
|
|
87
|
+
// Runs after every render (no dep-array) so it reacts to any value change,
|
|
88
|
+
// including programmatic resets (e.g. react-hook-form reset()).
|
|
89
|
+
// Guards against infinite loops by only scheduling a state update when the
|
|
90
|
+
// row count actually changes — React will bail out of the re-render when the
|
|
91
|
+
// new state value is identical to the current one.
|
|
86
92
|
useLayoutEffect(() => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const el = textareaRef.current;
|
|
94
|
+
if (!el)
|
|
95
|
+
return;
|
|
96
|
+
const savedRows = el.rows;
|
|
97
|
+
el.rows = 1; // collapse to measure natural scrollHeight
|
|
98
|
+
const lineHeight = Number.parseInt(getComputedStyle(el).lineHeight, 10) || 20;
|
|
99
|
+
const rawRows = Math.ceil(el.scrollHeight / lineHeight);
|
|
100
|
+
el.rows = savedRows; // restore before React paints
|
|
101
|
+
const next = Math.max(resolvedMinRows, maxRows !== undefined ? Math.min(rawRows, maxRows) : rawRows);
|
|
102
|
+
setRowCount((prev) => (prev === next ? prev : next));
|
|
103
|
+
});
|
|
92
104
|
// Integrated with react-hook-form
|
|
93
105
|
if (formContext) {
|
|
94
106
|
return (_jsx(Controller, { name: name, control: formContext.control, rules: {
|
|
@@ -101,13 +113,9 @@ export const TextAreaInput = ({ name, label, required = false, maxLength, contro
|
|
|
101
113
|
: undefined,
|
|
102
114
|
}, render: ({ field }) => (_jsx(Form.Control, { ...field, onChange: (e) => {
|
|
103
115
|
field.onChange(e);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}, ref: textareaRef, style: {
|
|
107
|
-
minHeight: MIN_TEXTAREA_HEIGHT,
|
|
108
|
-
resize: 'none',
|
|
109
|
-
}, as: "textarea", required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText, isInvalid: isInvalid })) }));
|
|
116
|
+
_handleTextAreaChange(e);
|
|
117
|
+
}, ref: textareaRef, className: "rhi-textarea-input", as: "textarea", rows: rowCount, required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText, isInvalid: isInvalid })) }));
|
|
110
118
|
}
|
|
111
119
|
// Standalone mode
|
|
112
|
-
return (_jsx(Form.Control, { as: "textarea", required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, value: value || '', disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText }));
|
|
120
|
+
return (_jsx(Form.Control, { as: "textarea", ref: textareaRef, className: "rhi-textarea-input", rows: rowCount, required: required, maxLength: maxLength, size: controlSize, placeholder: placeholder, value: value || '', onChange: _handleTextAreaChange, disabled: disabled, readOnly: isReadOnly, plaintext: isPlainText }));
|
|
113
121
|
};
|